first commit
This commit is contained in:
@@ -0,0 +1,905 @@
|
||||
import EditorLeftMenu from '../../../../frontend/js/features/editor-left-menu/components/editor-left-menu'
|
||||
import {
|
||||
AllowedImageName,
|
||||
OverallThemeMeta,
|
||||
SpellCheckLanguage,
|
||||
} from '../../../../types/project-settings'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { mockScope } from './scope'
|
||||
import { Folder } from '../../../../types/folder'
|
||||
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
describe('<EditorLeftMenu />', function () {
|
||||
beforeEach(function () {
|
||||
cy.viewport(800, 800)
|
||||
cy.interceptCompile()
|
||||
})
|
||||
|
||||
describe('for non-anonymous users', function () {
|
||||
const overallThemes: OverallThemeMeta[] = [
|
||||
{
|
||||
name: 'Overall Theme 1',
|
||||
val: '',
|
||||
path: 'https://overleaf.com/overalltheme-1.css',
|
||||
},
|
||||
{
|
||||
name: 'Overall Theme 2',
|
||||
val: 'light-',
|
||||
path: 'https://overleaf.com/overalltheme-2.css',
|
||||
},
|
||||
]
|
||||
|
||||
const allowedImageNames: AllowedImageName[] = [
|
||||
{
|
||||
imageDesc: 'Image 1',
|
||||
imageName: 'img-1',
|
||||
},
|
||||
{
|
||||
imageDesc: 'Image 2',
|
||||
imageName: 'img-2',
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-overallThemes', overallThemes)
|
||||
window.metaAttributesCache.set('ol-allowedImageNames', allowedImageNames)
|
||||
window.metaAttributesCache.set('ol-anonymous', false)
|
||||
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
|
||||
window.metaAttributesCache.set('ol-showSupport', true)
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { ieeeBrandId: 123 })
|
||||
window.metaAttributesCache.set('ol-user', {
|
||||
email: 'sherlock@holmes.co.uk',
|
||||
first_name: 'Sherlock',
|
||||
last_name: 'Holmes',
|
||||
})
|
||||
})
|
||||
|
||||
it('render full menu', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// Download Menu
|
||||
cy.findByRole('heading', { name: 'Download' })
|
||||
cy.findByRole('link', { name: 'Source' })
|
||||
cy.findByRole('link', { name: 'PDF' })
|
||||
|
||||
// Actions Menu
|
||||
cy.findByRole('heading', { name: 'Actions' })
|
||||
cy.findByRole('button', { name: 'Copy Project' })
|
||||
cy.findByRole('button', { name: 'Word Count' })
|
||||
|
||||
// Sync Menu
|
||||
cy.findByRole('heading', { name: 'Sync' })
|
||||
cy.findByRole('button', { name: 'Dropbox' })
|
||||
cy.findByRole('button', { name: 'Git' })
|
||||
cy.findByRole('button', { name: 'GitHub' })
|
||||
|
||||
// Settings Menu
|
||||
cy.findByRole('heading', { name: 'Settings' })
|
||||
cy.findByLabelText('Compiler')
|
||||
cy.findByLabelText('TeX Live version')
|
||||
cy.findByLabelText('Main document')
|
||||
cy.findByLabelText('Spell check')
|
||||
cy.findByLabelText('Auto-complete')
|
||||
cy.findByLabelText('Auto-close brackets')
|
||||
cy.findByLabelText('Code check')
|
||||
cy.findByLabelText('Editor theme')
|
||||
cy.findByLabelText('Overall theme')
|
||||
cy.findByLabelText('Keybindings')
|
||||
cy.findByLabelText('Font Size')
|
||||
cy.findByLabelText('Font Family')
|
||||
cy.findByLabelText('Line Height')
|
||||
cy.findByLabelText('PDF Viewer')
|
||||
|
||||
// Help Menu
|
||||
cy.findByRole('heading', { name: 'Help' })
|
||||
cy.findByRole('button', { name: 'Show Hotkeys' })
|
||||
cy.findByRole('link', { name: 'Documentation' })
|
||||
cy.findByRole('button', { name: 'Contact Us' })
|
||||
})
|
||||
|
||||
describe('download menu', function () {
|
||||
it('have a correct source & pdf download url', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('link', { name: 'Source' }).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'/project/project123/download/zip'
|
||||
)
|
||||
|
||||
cy.findByRole('link', { name: 'PDF' })
|
||||
.should('have.attr', 'href')
|
||||
.and('match', /\/download\/project\/project123\/build/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions menu', function () {
|
||||
it('shows copy project modal correctly', function () {
|
||||
cy.intercept('POST', '/project/*/clone', {
|
||||
body: {
|
||||
project_id: 'new_project_id',
|
||||
},
|
||||
})
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Copy Project' }).click()
|
||||
cy.findByRole('heading', { name: 'Copy Project' })
|
||||
|
||||
// try closing & re-opening the modal with different methods
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
cy.findByRole('button', { name: 'Copy Project' }).click()
|
||||
cy.findByRole('button', { name: 'Cancel' }).click()
|
||||
cy.findByRole('button', { name: 'Copy Project' }).click()
|
||||
|
||||
cy.findByLabelText('New Name').focus()
|
||||
cy.findByLabelText('New Name').clear()
|
||||
cy.findByLabelText('New Name').type('Project Renamed')
|
||||
cy.get('#clone-project-form-name[value="Project Renamed"')
|
||||
})
|
||||
|
||||
it('shows word count modal correctly', function () {
|
||||
cy.intercept('GET', '/project/*/wordcount*', {
|
||||
texcount: {
|
||||
encode: 'ascii',
|
||||
textWords: 781,
|
||||
headWords: 66,
|
||||
outside: 11,
|
||||
headers: 41,
|
||||
elements: 2,
|
||||
mathInline: 6,
|
||||
mathDisplay: 1,
|
||||
errors: 0,
|
||||
},
|
||||
}).as('wordCount')
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Word Count' }).click()
|
||||
|
||||
cy.wait('@wordCount')
|
||||
cy.findByText('Total Words:')
|
||||
cy.findByText('781')
|
||||
cy.findByText('Headers:')
|
||||
cy.findByText('41')
|
||||
cy.findByText('Math Inline:')
|
||||
cy.findByText('6')
|
||||
cy.findByText('Math Display:')
|
||||
cy.findByText('1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sync menu', function () {
|
||||
it('shows dropbox modal correctly', function () {
|
||||
cy.intercept('GET', '/dropbox/status', {
|
||||
registered: true,
|
||||
})
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
project: {
|
||||
members: [],
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
features: {
|
||||
dropbox: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Dropbox' }).click()
|
||||
cy.findByText('Dropbox Sync')
|
||||
})
|
||||
|
||||
it('shows git modal correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
project: {
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
features: {
|
||||
gitBridge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Git' }).click()
|
||||
cy.findByText('Clone with Git')
|
||||
cy.findByText(/clone your project by using the link below/)
|
||||
})
|
||||
|
||||
it('shows git modal paywall correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
project: {
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
features: {
|
||||
gitBridge: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Git' }).click()
|
||||
cy.findByText('Collaborate online and offline, using your own workflow')
|
||||
})
|
||||
|
||||
it('shows github modal correctly', function () {
|
||||
cy.intercept('GET', '/user/github-sync/status', {
|
||||
available: false,
|
||||
enabled: false,
|
||||
}).as('user-status')
|
||||
|
||||
cy.intercept('GET', '/project/*/github-sync/status', {
|
||||
enabled: false,
|
||||
}).as('project-status')
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wait('@compile')
|
||||
cy.findByRole('button', { name: 'GitHub' }).click()
|
||||
cy.findByText('GitHub Sync')
|
||||
|
||||
cy.wait(['@user-status', '@project-status'])
|
||||
cy.findByText('Push to GitHub, pull to Overleaf')
|
||||
})
|
||||
|
||||
it('hides the entire sync section when git bridge is disabled', function () {
|
||||
window.metaAttributesCache.set('ol-gitBridgeEnabled', false)
|
||||
|
||||
cy.findByRole('button', { name: 'Dropbox' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Git' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'GitHub' }).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('settings menu', function () {
|
||||
it('shows compiler menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-compiler option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq([
|
||||
'pdflatex',
|
||||
'latex',
|
||||
'xelatex',
|
||||
'lualatex',
|
||||
])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq([
|
||||
'pdfLaTeX',
|
||||
'LaTeX',
|
||||
'XeLaTeX',
|
||||
'LuaLaTeX',
|
||||
])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows texlive version menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-imageName option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['img-1', 'img-2'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['Image 1', 'Image 2'])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows document menu correctly', function () {
|
||||
const rootFolder: Folder = {
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: 'id1',
|
||||
name: 'main.tex',
|
||||
},
|
||||
{
|
||||
_id: 'id2',
|
||||
name: 'main2.tex',
|
||||
},
|
||||
],
|
||||
fileRefs: [],
|
||||
folders: [],
|
||||
}
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} rootFolder={[rootFolder as any]}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
const docs = docsInFolder(rootFolder)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-rootDocId option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(docs.map(doc => doc.doc.id))
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(docs.map(doc => doc.path))
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows spellcheck menu correctly', function () {
|
||||
const languages: SpellCheckLanguage[] = [
|
||||
{
|
||||
name: 'Lang 1',
|
||||
code: 'lang-1',
|
||||
dic: 'lang_1',
|
||||
},
|
||||
{
|
||||
name: 'Lang 2',
|
||||
code: 'lang-2',
|
||||
dic: 'lang_2',
|
||||
},
|
||||
]
|
||||
|
||||
window.metaAttributesCache.set('ol-languages', languages)
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>(
|
||||
'#settings-menu-spellCheckLanguage option'
|
||||
).then(options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['', 'lang-1', 'lang-2'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['Off', 'Lang 1', 'Lang 2'])
|
||||
})
|
||||
})
|
||||
|
||||
it('shows dictionary modal correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get('label[for="dictionary-settings"] ~ button').click()
|
||||
cy.findByText('Edit Dictionary')
|
||||
cy.findByText('Your custom dictionary is empty.')
|
||||
})
|
||||
|
||||
it('shows auto-complete menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-autoComplete option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['true', 'false'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['On', 'Off'])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows auto-close brackets menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>(
|
||||
'#settings-menu-autoPairDelimiters option'
|
||||
).then(options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['true', 'false'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['On', 'Off'])
|
||||
})
|
||||
})
|
||||
|
||||
it('shows code check menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>(
|
||||
'#settings-menu-syntaxValidation option'
|
||||
).then(options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['true', 'false'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['On', 'Off'])
|
||||
})
|
||||
})
|
||||
|
||||
it('shows editor theme menu correctly', function () {
|
||||
const editorThemes = ['editortheme-1', 'editortheme-2', 'editortheme-3']
|
||||
|
||||
const legacyEditorThemes = [
|
||||
'legacytheme-1',
|
||||
'legacytheme-2',
|
||||
'legacytheme-3',
|
||||
]
|
||||
|
||||
window.metaAttributesCache.set('ol-editorThemes', editorThemes)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-legacyEditorThemes',
|
||||
legacyEditorThemes
|
||||
)
|
||||
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-editorTheme option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq([
|
||||
'editortheme-1',
|
||||
'editortheme-2',
|
||||
'editortheme-3',
|
||||
'-',
|
||||
'legacytheme-1',
|
||||
'legacytheme-2',
|
||||
'legacytheme-3',
|
||||
])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq([
|
||||
'editortheme-1',
|
||||
'editortheme-2',
|
||||
'editortheme-3',
|
||||
'—————————————————',
|
||||
'legacytheme-1 (Legacy)',
|
||||
'legacytheme-2 (Legacy)',
|
||||
'legacytheme-3 (Legacy)',
|
||||
])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows overall theme menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-overallTheme option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['', 'light-'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['Overall Theme 1', 'Overall Theme 2'])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows keybindings menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-mode option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['default', 'vim', 'emacs'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['None', 'Vim', 'Emacs'])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows font size menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-fontSize option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq([
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'16',
|
||||
'18',
|
||||
'20',
|
||||
'22',
|
||||
'24',
|
||||
])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq([
|
||||
'10px',
|
||||
'11px',
|
||||
'12px',
|
||||
'13px',
|
||||
'14px',
|
||||
'16px',
|
||||
'18px',
|
||||
'20px',
|
||||
'22px',
|
||||
'24px',
|
||||
])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows font family menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-fontFamily option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['monaco', 'lucida', 'opendyslexicmono'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq([
|
||||
'Monaco / Menlo / Consolas',
|
||||
'Lucida / Source Code Pro',
|
||||
'OpenDyslexic Mono',
|
||||
])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows line height menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-lineHeight option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['compact', 'normal', 'wide'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['Compact', 'Normal', 'Wide'])
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('shows pdf viewer menu correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.get<HTMLOptionElement>('#settings-menu-pdfViewer option').then(
|
||||
options => {
|
||||
const values = [...options].map(o => o.value)
|
||||
expect(values).to.deep.eq(['pdfjs', 'native'])
|
||||
|
||||
const texts = [...options].map(o => o.text)
|
||||
expect(texts).to.deep.eq(['Overleaf', 'Browser'])
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('help menu', function () {
|
||||
it('shows hotkeys modal correctly', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Show Hotkeys' }).click()
|
||||
cy.findByText('Hotkeys')
|
||||
})
|
||||
|
||||
it('shows correct url for documentation', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('link', { name: 'Documentation' }).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'/learn'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows correct contact us modal', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Contact Us' }).click()
|
||||
cy.findByText('Affected project URL (Optional)')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('for anonymous users', function () {
|
||||
it('render minimal menu', function () {
|
||||
const scope = mockScope({
|
||||
ui: {
|
||||
leftMenuShown: true,
|
||||
},
|
||||
})
|
||||
window.metaAttributesCache.set('ol-anonymous', true)
|
||||
Object.assign(getMeta('ol-ExposedSettings'), { ieeeBrandId: 123 })
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// Download Menu
|
||||
cy.findByRole('heading', { name: 'Download' })
|
||||
cy.findByRole('link', { name: 'Source' })
|
||||
cy.findByRole('link', { name: 'PDF' })
|
||||
|
||||
// Actions Menu
|
||||
cy.findByRole('heading', { name: 'Actions' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Copy Project' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Word Count' }).should('not.exist')
|
||||
|
||||
// Sync Menu
|
||||
cy.findByRole('heading', { name: 'Sync' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Dropbox' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'Git' }).should('not.exist')
|
||||
cy.findByRole('button', { name: 'GitHub' }).should('not.exist')
|
||||
|
||||
// Settings Menu
|
||||
cy.findByRole('heading', { name: 'Settings' }).should('not.exist')
|
||||
cy.findByLabelText('Compiler').should('not.exist')
|
||||
cy.findByLabelText('TeX Live version').should('not.exist')
|
||||
cy.findByLabelText('Main document').should('not.exist')
|
||||
cy.findByLabelText('Spell check').should('not.exist')
|
||||
cy.findByLabelText('Auto-complete').should('not.exist')
|
||||
cy.findByLabelText('Auto-close brackets').should('not.exist')
|
||||
cy.findByLabelText('Code check').should('not.exist')
|
||||
cy.findByLabelText('Editor theme').should('not.exist')
|
||||
cy.findByLabelText('Overall theme').should('not.exist')
|
||||
cy.findByLabelText('Keybindings').should('not.exist')
|
||||
cy.findByLabelText('Font Size').should('not.exist')
|
||||
cy.findByLabelText('Font Family').should('not.exist')
|
||||
cy.findByLabelText('Line Height').should('not.exist')
|
||||
cy.findByLabelText('PDF Viewer').should('not.exist')
|
||||
|
||||
// Help Menu
|
||||
cy.findByRole('heading', { name: 'Help' })
|
||||
cy.findByRole('button', { name: 'Show Hotkeys' })
|
||||
cy.findByRole('button', { name: 'Documentation' }).should('not.exist')
|
||||
cy.findByRole('link', { name: 'Contact Us' }).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,56 @@
|
||||
import { MainDocument } from '../../../../types/project-settings'
|
||||
import { PdfViewer } from '../../../../types/user-settings'
|
||||
|
||||
type Scope = {
|
||||
settings?: {
|
||||
syntaxValidation?: boolean
|
||||
pdfViewer?: PdfViewer
|
||||
}
|
||||
editor?: {
|
||||
sharejs_doc?: {
|
||||
doc_id?: string
|
||||
getSnapshot?: () => string
|
||||
}
|
||||
}
|
||||
hasLintingError?: boolean
|
||||
ui?: {
|
||||
view?: 'editor' | 'history' | 'file' | 'pdf'
|
||||
pdfLayout?: 'flat' | 'sideBySide' | 'split'
|
||||
leftMenuShown?: boolean
|
||||
}
|
||||
project?: {
|
||||
members?: any[]
|
||||
owner: {
|
||||
_id: string
|
||||
}
|
||||
features?: {
|
||||
gitBridge?: boolean
|
||||
}
|
||||
}
|
||||
user?: {
|
||||
features?: {
|
||||
dropbox: boolean
|
||||
}
|
||||
}
|
||||
docs?: MainDocument[]
|
||||
}
|
||||
|
||||
export const mockScope = (scope?: Scope) => ({
|
||||
settings: {
|
||||
syntaxValidation: false,
|
||||
pdfViewer: 'pdfjs',
|
||||
},
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
},
|
||||
},
|
||||
hasLintingError: false,
|
||||
ui: {
|
||||
view: 'editor',
|
||||
pdfLayout: 'sideBySide',
|
||||
leftMenuShown: false,
|
||||
},
|
||||
...scope,
|
||||
})
|
@@ -0,0 +1,84 @@
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import DetachCompileButtonWrapper from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button-wrapper'
|
||||
import { mockScope } from './scope'
|
||||
import { testDetachChannel } from '../../helpers/detach-channel'
|
||||
|
||||
describe('<DetachCompileButtonWrapper />', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('detacher mode and not linked: does not show button ', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<DetachCompileButtonWrapper />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('detacher mode and linked: show button', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<DetachCompileButtonWrapper />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'connected',
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
it('not detacher mode and linked: does not show button ', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<DetachCompileButtonWrapper />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
|
||||
})
|
||||
})
|
@@ -0,0 +1,103 @@
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import PdfJsViewer from '../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
|
||||
import { mockScope } from './scope'
|
||||
import { getContainerEl } from 'cypress/react'
|
||||
import { unmountComponentAtNode } from 'react-dom'
|
||||
import { PdfPreviewProvider } from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-provider'
|
||||
|
||||
// Unicode directional isolates, added around placeables by @fluent/bundle/esm/resolver
|
||||
const FSI = '\u2068'
|
||||
const PDI = '\u2069'
|
||||
|
||||
describe('<PdfJSViewer/>', function () {
|
||||
beforeEach(function () {
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('loads all PDF pages', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfPreviewProvider>
|
||||
<div className="pdf-viewer">
|
||||
<PdfJsViewer url="/build/123/output.pdf?clsiserverid=foo" />
|
||||
</div>
|
||||
</PdfPreviewProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('region', { name: `Page ${FSI}1${PDI}` })
|
||||
cy.findByRole('region', { name: `Page ${FSI}2${PDI}` })
|
||||
cy.findByRole('region', { name: `Page ${FSI}3${PDI}` })
|
||||
cy.findByRole('region', { name: `Page ${FSI}4${PDI}` }).should('not.exist')
|
||||
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
|
||||
it('renders pages in a "loading" state', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfPreviewProvider>
|
||||
<div className="pdf-viewer">
|
||||
<PdfJsViewer url="/build/123/output.pdf" />
|
||||
</div>
|
||||
</PdfPreviewProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.get('.page.loading')
|
||||
})
|
||||
|
||||
it('can be unmounted while loading a document', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfPreviewProvider>
|
||||
<div className="pdf-viewer">
|
||||
<PdfJsViewer url="/build/123/output.pdf?clsiserverid=foo" />
|
||||
</div>
|
||||
</PdfPreviewProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.then(() => unmountComponentAtNode(getContainerEl()))
|
||||
})
|
||||
|
||||
it('can be unmounted after loading a document', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfPreviewProvider>
|
||||
<div className="pdf-viewer">
|
||||
<PdfJsViewer url="/build/123/output.pdf?clsiserverid=foo" />
|
||||
</div>
|
||||
</PdfPreviewProvider>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('region', { name: `Page ${FSI}1${PDI}` })
|
||||
|
||||
cy.then(() => unmountComponentAtNode(getContainerEl()))
|
||||
})
|
||||
})
|
@@ -0,0 +1,186 @@
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import PdfLogsEntries from '../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
import { FC } from 'react'
|
||||
import {
|
||||
EditorManager,
|
||||
EditorManagerContext,
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
|
||||
import { LogEntry } from '@/features/pdf-preview/util/types'
|
||||
|
||||
describe('<PdfLogsEntries/>', function () {
|
||||
const fakeFindEntityResult: FindResult = {
|
||||
type: 'doc',
|
||||
entity: { _id: '123', name: '123 Doc' },
|
||||
}
|
||||
|
||||
const FileTreePathProvider: FC = ({ children }) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
dirname: cy.stub(),
|
||||
findEntityByPath: cy
|
||||
.stub()
|
||||
.as('findEntityByPath')
|
||||
.returns(fakeFindEntityResult),
|
||||
pathInFolder: cy.stub(),
|
||||
previewByPath: cy.stub(),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
const EditorManagerProvider: FC = ({ children }) => {
|
||||
const value = {
|
||||
openDocWithId: cy.spy().as('openDocWithId'),
|
||||
// @ts-ignore
|
||||
openDocs: new OpenDocuments(),
|
||||
} as unknown as EditorManager
|
||||
|
||||
return (
|
||||
<EditorManagerContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorManagerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const logEntries: LogEntry[] = [
|
||||
{
|
||||
file: 'main.tex',
|
||||
line: 9,
|
||||
column: 8,
|
||||
level: 'error',
|
||||
message: 'LaTeX Error',
|
||||
content: 'See the LaTeX manual',
|
||||
raw: '',
|
||||
ruleId: 'hint_misplaced_alignment_tab_character',
|
||||
key: '',
|
||||
},
|
||||
]
|
||||
|
||||
const scope = {
|
||||
'editor.view': new EditorView({ doc: '\\documentclass{article}' }),
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.interceptCompile()
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('displays human readable hint', function () {
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfLogsEntries entries={logEntries} />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.contains('You have placed an alignment tab character')
|
||||
})
|
||||
|
||||
it('opens doc on click', function () {
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ EditorManagerProvider, FileTreePathProvider }}
|
||||
>
|
||||
<PdfLogsEntries entries={logEntries} />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Navigate to log position in source code: main.tex, 9',
|
||||
}).click()
|
||||
|
||||
cy.get('@findEntityByPath').should('have.been.calledOnceWith', 'main.tex')
|
||||
cy.get('@openDocWithId').should(
|
||||
'have.been.calledOnceWith',
|
||||
fakeFindEntityResult.entity._id,
|
||||
{
|
||||
gotoLine: 9,
|
||||
gotoColumn: 8,
|
||||
keepCurrentView: false,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('opens doc via detached action', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ EditorManagerProvider, FileTreePathProvider }}
|
||||
>
|
||||
<PdfLogsEntries entries={logEntries} />
|
||||
</EditorProviders>
|
||||
).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'action-sync-to-entry',
|
||||
data: {
|
||||
args: [
|
||||
{
|
||||
file: 'main.tex',
|
||||
line: 7,
|
||||
column: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('@findEntityByPath').should('have.been.calledOnce')
|
||||
cy.get('@openDocWithId').should(
|
||||
'have.been.calledOnceWith',
|
||||
fakeFindEntityResult.entity._id,
|
||||
{
|
||||
gotoLine: 7,
|
||||
gotoColumn: 6,
|
||||
keepCurrentView: false,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('sends open doc clicks via detached action', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ EditorManagerProvider, FileTreePathProvider }}
|
||||
>
|
||||
<PdfLogsEntries entries={logEntries} />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Navigate to log position in source code: main.tex, 9',
|
||||
}).click()
|
||||
|
||||
cy.get('@findEntityByPath').should('not.have.been.called')
|
||||
cy.get('@openDocWithId').should('not.have.been.called')
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detached',
|
||||
event: 'action-sync-to-entry',
|
||||
data: {
|
||||
args: [
|
||||
{
|
||||
file: 'main.tex',
|
||||
line: 9,
|
||||
column: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,73 @@
|
||||
import PdfPreviewDetachedRoot from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
|
||||
describe('<PdfPreviewDetachedRoot/>', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-user', { id: 'user1' })
|
||||
window.metaAttributesCache.set('ol-project_id', 'project1')
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
window.metaAttributesCache.set('ol-projectName', 'Project Name')
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('syncs compiling state', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.mount(<PdfPreviewDetachedRoot />)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-compiling',
|
||||
data: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Compiling…' })
|
||||
cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-compiling',
|
||||
data: { value: false },
|
||||
})
|
||||
})
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
cy.findByRole('button', { name: 'Compiling…' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('sends a clear cache request when the button is pressed', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.mount(<PdfPreviewDetachedRoot />)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-showLogs',
|
||||
data: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
|
||||
cy.findByRole('button', { name: 'Clear cached files' })
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detached',
|
||||
event: 'action-clearCache',
|
||||
data: {
|
||||
args: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,100 @@
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import PdfPreviewHybridToolbar from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
|
||||
import { testDetachChannel } from '../../helpers/detach-channel'
|
||||
|
||||
describe('<PdfPreviewHybridToolbar/>', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('shows normal mode', function () {
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<PdfPreviewHybridToolbar />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
describe('orphan mode', function () {
|
||||
it('shows connecting message on load', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<PdfPreviewHybridToolbar />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.contains('Connecting with the editor')
|
||||
})
|
||||
|
||||
it('shows compile UI when connected', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<PdfPreviewHybridToolbar />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
it('shows connecting message when disconnected', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<PdfPreviewHybridToolbar />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'connected',
|
||||
})
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'closed',
|
||||
})
|
||||
})
|
||||
|
||||
cy.contains('Connecting with the editor')
|
||||
})
|
||||
|
||||
it('shows redirect button after timeout', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
cy.clock()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders>
|
||||
<PdfPreviewHybridToolbar />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.tick(6000)
|
||||
|
||||
cy.findByRole('button', { name: 'Redirect to editor' })
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,872 @@
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { mockScope } from './scope'
|
||||
import {
|
||||
IdeLayout,
|
||||
IdeView,
|
||||
useLayoutContext,
|
||||
} from '../../../../frontend/js/shared/context/layout-context'
|
||||
import { FC, useEffect } from 'react'
|
||||
|
||||
const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => {
|
||||
localStorage.setItem(key, value)
|
||||
win.dispatchEvent(new StorageEvent('storage', { key }))
|
||||
}
|
||||
|
||||
const Layout: FC<{ layout: IdeLayout; view?: IdeView }> = ({
|
||||
layout,
|
||||
view,
|
||||
}) => {
|
||||
const { changeLayout } = useLayoutContext()
|
||||
|
||||
useEffect(() => {
|
||||
changeLayout(layout, view)
|
||||
}, [changeLayout, layout, view])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
describe('<PdfPreview/>', function () {
|
||||
let projectId: string
|
||||
beforeEach(function () {
|
||||
/**
|
||||
* There are time sensitive tests in this test suite. They need to wait for a Promise before resolving a request.
|
||||
*
|
||||
* Using a promise across the test-env (browser) vs stub-env (server) causes additional latency.
|
||||
*
|
||||
* This latency seems to stack up when adding more intercepts for the same path. Using static responses for some of these intercepts does not help.
|
||||
*
|
||||
* All of that seems like a bug in Cypress. For now just work around it by using a unique projectId for each intercept.
|
||||
*/
|
||||
projectId = Math.random().toString().slice(2)
|
||||
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
|
||||
window.metaAttributesCache.set(
|
||||
'ol-compilesUserContentDomain',
|
||||
'https://compiles-user.dev-overleaf.com'
|
||||
)
|
||||
window.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'initial-compile-from-clsi-cache': 'enabled',
|
||||
})
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('renders the PDF preview', function () {
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// wait for "compile on load" to finish
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
it('uses the cache when available', function () {
|
||||
cy.interceptCompile({
|
||||
prefix: 'compile',
|
||||
times: 1,
|
||||
cached: true,
|
||||
regular: false,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// wait for "compile from cache on load" to finish
|
||||
cy.waitForCompile({ pdf: true, cached: true, regular: false })
|
||||
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
|
||||
it('uses the cache when available then compiles', function () {
|
||||
cy.interceptCompile({
|
||||
prefix: 'compile',
|
||||
times: 1,
|
||||
cached: true,
|
||||
regular: false,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// wait for "compile from cache on load" to finish
|
||||
cy.waitForCompile({ pdf: true, cached: true, regular: false })
|
||||
cy.contains('Your Paper')
|
||||
|
||||
// Then trigger a new compile
|
||||
cy.interceptCompile({
|
||||
prefix: 'recompile',
|
||||
times: 1,
|
||||
cached: false,
|
||||
outputPDFFixture: 'output-2.pdf',
|
||||
})
|
||||
|
||||
// press the Recompile button => compile
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ prefix: 'recompile', pdf: true })
|
||||
cy.contains('Modern Authoring Tools for Science')
|
||||
})
|
||||
|
||||
describe('racing compile from cache and regular compile trigger', function () {
|
||||
for (const [timing] of ['before rendering', 'after rendering']) {
|
||||
it(`replaces the compile from cache with a regular compile - ${timing}`, function () {
|
||||
const requestedOnce = new Set()
|
||||
;['log', 'pdf', 'blg'].forEach(ext => {
|
||||
cy.intercept({ pathname: `/build/*/output.${ext}` }, req => {
|
||||
if (requestedOnce.has(ext)) {
|
||||
throw new Error(
|
||||
`compile from cache triggered extra ${ext} request: ${req.url}`
|
||||
)
|
||||
}
|
||||
requestedOnce.add(ext)
|
||||
req.reply({ fixture: `build/output.${ext},null` })
|
||||
}).as(`compile-${ext}`)
|
||||
})
|
||||
const { promise, resolve } = Promise.withResolvers<void>()
|
||||
cy.interceptCompileFromCacheRequest({
|
||||
promise,
|
||||
times: 1,
|
||||
}).as('cached-compile')
|
||||
cy.interceptCompileRequest().as('compile')
|
||||
|
||||
const scope = mockScope()
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} projectId={projectId}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// press the Recompile button => compile
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
|
||||
if (timing === 'before rendering') {
|
||||
cy.then(() => resolve())
|
||||
cy.wait('@cached-compile')
|
||||
}
|
||||
|
||||
// wait for rendering to finish
|
||||
cy.waitForCompile({ pdf: true, cached: false })
|
||||
|
||||
if (timing === 'after rendering') {
|
||||
cy.then(() => resolve())
|
||||
cy.wait('@cached-compile')
|
||||
}
|
||||
|
||||
cy.contains('Your Paper')
|
||||
cy.then(() => Array.from(requestedOnce).sort().join(',')).should(
|
||||
'equal',
|
||||
'blg,log,pdf'
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('clsi-cache project settings validation', function () {
|
||||
const cases = {
|
||||
// Flaky, skip for now
|
||||
'uses compile from cache when nothing changed': {
|
||||
cached: true,
|
||||
setup: () => {},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when imageName changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
imageName: 'texlive-full:2025.1',
|
||||
},
|
||||
},
|
||||
'ignores the compile from cache when compiler changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
compiler: 'lualatex',
|
||||
},
|
||||
},
|
||||
'ignores the compile from cache when draft mode changed': {
|
||||
cached: false,
|
||||
setup: () => {
|
||||
cy.window().then(w =>
|
||||
w.localStorage.setItem(`draft:${projectId}`, 'true')
|
||||
)
|
||||
},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when stopOnFirstError mode changed': {
|
||||
cached: false,
|
||||
setup: () => {
|
||||
cy.window().then(w =>
|
||||
w.localStorage.setItem(`stop_on_first_error:${projectId}`, 'true')
|
||||
)
|
||||
},
|
||||
props: {},
|
||||
},
|
||||
'ignores the compile from cache when rootDoc changed': {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
rootDocId: 'new-root-doc-id',
|
||||
rootFolder: [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: '_root_doc_id',
|
||||
name: 'main.tex',
|
||||
},
|
||||
{
|
||||
_id: 'new-root-doc-id',
|
||||
name: 'new-main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
Object.entries(cases).forEach(([name, { cached, setup, props }]) => {
|
||||
it(name, function () {
|
||||
cy.interceptCompile({
|
||||
cached: true,
|
||||
regular: !cached,
|
||||
})
|
||||
|
||||
const scope = mockScope()
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
|
||||
setup()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} projectId={projectId} {...props}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ pdf: true, cached, regular: !cached })
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('runs a compile when the Recompile button is pressed', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// press the Recompile button => compile
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
|
||||
it('runs a compile on `pdf:recompile` event', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.window().then(win => {
|
||||
win.dispatchEvent(new CustomEvent('pdf:recompile'))
|
||||
})
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.contains('Your Paper')
|
||||
})
|
||||
|
||||
it('does not compile while compiling', function () {
|
||||
let counter = 0
|
||||
cy.interceptDeferredCompile(() => counter++).then(
|
||||
resolveDeferredCompile => {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
// start compiling
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Compiling…' }).then(() => {
|
||||
// trigger a recompile
|
||||
cy.window().then(win => {
|
||||
win.dispatchEvent(new CustomEvent('pdf:recompile'))
|
||||
})
|
||||
|
||||
// finish the original compile
|
||||
resolveDeferredCompile()
|
||||
|
||||
// wait for the original compile to finish
|
||||
cy.waitForCompile().then(() => {
|
||||
// NOTE: difficult to assert that a second request won't be sent, at some point
|
||||
expect(counter).to.equal(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('disables compile button while compile is running', function () {
|
||||
cy.interceptDeferredCompile().then(resolveDeferredCompile => {
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.findByRole('button', { name: 'Compiling…' })
|
||||
.should('be.disabled')
|
||||
.then(() => resolveDeferredCompile())
|
||||
|
||||
cy.waitForCompile()
|
||||
cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled')
|
||||
})
|
||||
})
|
||||
|
||||
it('runs a compile on doc change if autocompile is enabled', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.clock()
|
||||
|
||||
// switch on auto compile
|
||||
storeAndFireEvent(win, 'autocompile_enabled:project123', true)
|
||||
|
||||
// fire a doc:changed event => compile
|
||||
win.dispatchEvent(new CustomEvent('doc:changed'))
|
||||
|
||||
// wait enough time for the compile to start
|
||||
cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
|
||||
|
||||
cy.clock().invoke('restore')
|
||||
})
|
||||
|
||||
// wait for compile to finish
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
it('does not run a compile on doc change if autocompile is disabled', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.clock()
|
||||
|
||||
// make sure auto compile is switched off
|
||||
storeAndFireEvent(win, 'autocompile_enabled:project123', false)
|
||||
|
||||
// fire a doc:changed event => no compile
|
||||
win.dispatchEvent(new CustomEvent('doc:changed'))
|
||||
|
||||
// wait enough time for the compile to start
|
||||
cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
|
||||
|
||||
cy.clock().invoke('restore')
|
||||
})
|
||||
|
||||
// NOTE: difficult to assert that a request hasn't been sent
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
it('does not run a compile on doc change if autocompile is blocked by syntax check', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
// enable linting in the editor
|
||||
const userSettings = { syntaxValidation: true }
|
||||
// mock a linting error
|
||||
scope.hasLintingError = true
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} userSettings={userSettings}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.clock()
|
||||
|
||||
// switch on auto compile
|
||||
storeAndFireEvent(win, 'autocompile_enabled:project123', true)
|
||||
|
||||
// switch on syntax checking
|
||||
storeAndFireEvent(win, 'stop_on_validation_error', true)
|
||||
|
||||
// fire a doc:changed event => no compile
|
||||
win.dispatchEvent(new CustomEvent('doc:changed'))
|
||||
|
||||
// wait enough time for the compile to start
|
||||
cy.tick(6000) // AUTO_COMPILE_DEBOUNCE
|
||||
|
||||
cy.clock().invoke('restore')
|
||||
})
|
||||
|
||||
// NOTE: difficult to assert that a request hasn't been sent
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
|
||||
cy.findByText('Code check failed')
|
||||
})
|
||||
|
||||
it('does not run a compile on doc change if the PDF preview is not open', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<Layout layout="flat" view="editor" />
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.clock()
|
||||
|
||||
// switch on auto compile
|
||||
storeAndFireEvent(win, 'autocompile_enabled:project123', true)
|
||||
|
||||
// fire a doc:changed event => compile
|
||||
win.dispatchEvent(new CustomEvent('doc:changed'))
|
||||
|
||||
// wait enough time for the compile to start
|
||||
cy.tick(6000) // > AUTO_COMPILE_DEBOUNCE
|
||||
|
||||
cy.clock().invoke('restore')
|
||||
})
|
||||
|
||||
// NOTE: difficult to assert that a request hasn't been sent
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
|
||||
describe('error messages', function () {
|
||||
const compileErrorStatuses = {
|
||||
'clear-cache':
|
||||
'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
|
||||
'clsi-maintenance':
|
||||
'The compile servers are down for maintenance, and will be back shortly.',
|
||||
'compile-in-progress':
|
||||
'A previous compile is still running. Please wait a minute and try compiling again.',
|
||||
exited: 'Server Error',
|
||||
failure: 'No PDF',
|
||||
generic: 'Server Error',
|
||||
'project-too-large': 'Project too large',
|
||||
'rate-limited': 'Compile rate limit hit',
|
||||
terminated: 'Compilation cancelled',
|
||||
timedout: 'Timed out',
|
||||
'too-recently-compiled':
|
||||
'This project was compiled very recently, so this compile has been skipped.',
|
||||
unavailable:
|
||||
'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.',
|
||||
foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.',
|
||||
}
|
||||
|
||||
for (const [status, message] of Object.entries(compileErrorStatuses)) {
|
||||
it(`displays error message for '${status}' status`, function () {
|
||||
cy.intercept('POST', '/project/*/compile*', {
|
||||
body: {
|
||||
status,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
}).as('compile')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.wait('@compile')
|
||||
cy.findByText(message)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('displays expandable raw logs', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('button', { name: 'View logs' }).click()
|
||||
cy.findByRole('button', { name: 'View PDF' })
|
||||
|
||||
cy.findByRole('button', { name: 'Expand' }).click()
|
||||
cy.findByRole('button', { name: 'Collapse' }).click()
|
||||
})
|
||||
|
||||
it('displays error messages if there were validation problems', function () {
|
||||
const validationProblems = {
|
||||
sizeCheck: {
|
||||
resources: [
|
||||
{ path: 'foo/bar', kbSize: 76221 },
|
||||
{ path: 'bar/baz', kbSize: 2342 },
|
||||
],
|
||||
},
|
||||
mainFile: true,
|
||||
conflictedPaths: [
|
||||
{
|
||||
path: 'foo/bar',
|
||||
},
|
||||
{
|
||||
path: 'foo/baz',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
cy.intercept('POST', '/project/*/compile*', {
|
||||
body: {
|
||||
status: 'validation-problems',
|
||||
validationProblems,
|
||||
clsiServerId: 'foo',
|
||||
compileGroup: 'priority',
|
||||
},
|
||||
}).as('compile')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.wait('@compile')
|
||||
|
||||
cy.findByText('Project too large')
|
||||
cy.findByText('Unknown main document')
|
||||
cy.findByText('Conflicting Paths Found')
|
||||
})
|
||||
|
||||
describe('clear cache', function () {
|
||||
it('sends a clear cache request when the button is pressed', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('button', { name: 'View logs' }).click()
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<void>()
|
||||
|
||||
cy.intercept('DELETE', '/project/*/output*', req => {
|
||||
return promise
|
||||
.then(() => Cypress.Promise.delay(100))
|
||||
.then(() => {
|
||||
req.reply({ statusCode: 204 })
|
||||
})
|
||||
}).as('clear-cache')
|
||||
|
||||
// click the button
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).click()
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
cy.then(() => {
|
||||
resolve()
|
||||
})
|
||||
cy.wait('@clear-cache')
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
})
|
||||
|
||||
it('handle "recompile from scratch"', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile({ pdf: true })
|
||||
cy.interceptCompile({ prefix: 'recompile' })
|
||||
|
||||
cy.intercept('DELETE', '/project/*/output*', {
|
||||
statusCode: 204,
|
||||
delay: 100,
|
||||
}).as('clear-cache')
|
||||
|
||||
// show the logs UI
|
||||
cy.findByRole('button', { name: 'View logs' }).click()
|
||||
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
cy.interceptDeferredCompile().then(resolveDeferredCompile => {
|
||||
cy.findByRole('button', { name: 'Toggle compile options menu' }).click()
|
||||
|
||||
cy.findByRole('menuitem', {
|
||||
name: 'Recompile from scratch',
|
||||
}).trigger('click')
|
||||
|
||||
cy.findByRole('button', { name: 'Clear cached files' }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
|
||||
cy.wait('@clear-cache')
|
||||
|
||||
cy.findByRole('button', { name: 'Compiling…' }).then(() =>
|
||||
resolveDeferredCompile()
|
||||
)
|
||||
|
||||
// wait for recompile from scratch to finish
|
||||
cy.waitForCompile({ pdf: true })
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid URLs and broken PDFs', function () {
|
||||
it('shows an error for an invalid URL', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.intercept('/build/*/output.pdf*', {
|
||||
statusCode: 500,
|
||||
body: {
|
||||
message: 'something awful happened',
|
||||
code: 'AWFUL_ERROR',
|
||||
},
|
||||
}).as('compile-pdf-error')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile()
|
||||
cy.wait('@compile-pdf-error')
|
||||
|
||||
cy.contains('Something went wrong while rendering this PDF.')
|
||||
cy.contains(
|
||||
'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
|
||||
)
|
||||
cy.findByLabelText('Page 1').should('not.exist')
|
||||
})
|
||||
|
||||
it('shows an error for a corrupt PDF', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.intercept('/build/*/output.pdf*', {
|
||||
fixture: 'build/output-corrupt.pdf,null',
|
||||
}).as('compile-pdf-corrupt')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile()
|
||||
cy.wait('@compile-pdf-corrupt')
|
||||
|
||||
cy.contains('Something went wrong while rendering this PDF.')
|
||||
cy.contains(
|
||||
'Please try recompiling the project from scratch, and if that doesn’t help, follow our troubleshooting guide.'
|
||||
)
|
||||
cy.findByLabelText('Page 1').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('human readable logs', function () {
|
||||
it('shows human readable hint for undefined reference errors', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
cy.intercept('/build/*/output.log*', {
|
||||
fixture: 'build/output-human-readable.log',
|
||||
}).as('compile-log')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile()
|
||||
cy.findByRole('button', { name: 'View logs' }).click()
|
||||
|
||||
cy.findByText(
|
||||
"Reference `intorduction' on page 1 undefined on input line 11."
|
||||
)
|
||||
cy.findByText(
|
||||
"Reference `section1' on page 1 undefined on input line 13."
|
||||
)
|
||||
cy.findByText('There were undefined references.')
|
||||
|
||||
cy.findAllByText(
|
||||
/You have referenced something which has not yet been labelled/
|
||||
).should('have.length', 3)
|
||||
})
|
||||
|
||||
it('does not show human readable hint when no undefined reference errors', function () {
|
||||
cy.interceptCompile()
|
||||
cy.intercept('/build/*/output.log?*', {
|
||||
fixture: 'build/output-undefined-references.log',
|
||||
}).as('compile-log')
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfPreview />
|
||||
</div>
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Recompile' }).click()
|
||||
cy.waitForCompile()
|
||||
cy.findByRole('button', { name: 'View logs' }).click()
|
||||
|
||||
cy.findByText(
|
||||
"Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right."
|
||||
)
|
||||
|
||||
cy.findByText(
|
||||
/You have referenced something which has not yet been labelled/
|
||||
).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,439 @@
|
||||
import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context'
|
||||
import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context'
|
||||
import { useEffect } from 'react'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { mockScope } from './scope'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 509.999878,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
h: 85.03936,
|
||||
v: 486.089539,
|
||||
width: 441.921265,
|
||||
height: 8.855677,
|
||||
},
|
||||
]
|
||||
|
||||
type Position = {
|
||||
page: number
|
||||
offset: { top: number; left: number }
|
||||
pageSize: { height: number; width: number }
|
||||
}
|
||||
|
||||
const mockPosition: Position = {
|
||||
page: 1,
|
||||
offset: { top: 10, left: 10 },
|
||||
pageSize: { height: 500, width: 500 },
|
||||
}
|
||||
|
||||
const mockSelectedEntities = [{ type: 'doc' }] as FindResult[]
|
||||
|
||||
const WithPosition = ({ mockPosition }: { mockPosition: Position }) => {
|
||||
const { setPosition } = useCompileContext()
|
||||
|
||||
// mock PDF scroll position update
|
||||
useEffect(() => {
|
||||
setPosition(mockPosition)
|
||||
}, [mockPosition, setPosition])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// mock PDF scroll position update
|
||||
const setDetachedPosition = (mockPosition: Position) => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-position',
|
||||
data: { value: mockPosition },
|
||||
})
|
||||
}
|
||||
|
||||
const WithSelectedEntities = ({
|
||||
mockSelectedEntities = [],
|
||||
}: {
|
||||
mockSelectedEntities: FindResult[]
|
||||
}) => {
|
||||
const { setSelectedEntities } = useFileTreeData()
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEntities(mockSelectedEntities)
|
||||
}, [mockSelectedEntities, setSelectedEntities])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
describe('<PdfSynctexControls/>', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-project_id', 'test-project')
|
||||
window.metaAttributesCache.set('ol-preventCompileOnLoad', false)
|
||||
cy.interceptEvents()
|
||||
})
|
||||
|
||||
it('handles clicks on sync buttons', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.get('.synctex-control-icon').should('have.length', 2)
|
||||
|
||||
// mock editor cursor position update
|
||||
cy.window().then(win => {
|
||||
win.dispatchEvent(
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
setDetachedPosition(mockPosition)
|
||||
})
|
||||
|
||||
cy.interceptAsync({ pathname: '/project/*/sync/code' }, 'sync-code').then(
|
||||
syncCodeResponse => {
|
||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
|
||||
cy.findByRole('button', { name: 'Go to code location in PDF' })
|
||||
.should('be.disabled')
|
||||
.then(() => {
|
||||
syncCodeResponse.resolve({
|
||||
body: { pdf: cloneDeep(mockHighlights) },
|
||||
})
|
||||
})
|
||||
|
||||
cy.wait('@sync-code')
|
||||
}
|
||||
)
|
||||
|
||||
cy.interceptAsync({ pathname: '/project/*/sync/pdf' }, 'sync-pdf').then(
|
||||
syncPdfResponse => {
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ })
|
||||
.should('be.disabled')
|
||||
.then(() => {
|
||||
syncPdfResponse.resolve({
|
||||
body: { code: [{ file: 'main.tex', line: 100 }] },
|
||||
})
|
||||
})
|
||||
|
||||
cy.wait('@sync-pdf')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('disables button when multiple entities are selected', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities
|
||||
mockSelectedEntities={
|
||||
[{ type: 'doc' }, { type: 'doc' }] as FindResult[]
|
||||
}
|
||||
/>
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
})
|
||||
|
||||
it('disables button when a file is selected', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities
|
||||
mockSelectedEntities={[{ type: 'fileRef' }] as FindResult[]}
|
||||
/>
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
})
|
||||
|
||||
describe('with detacher role', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detacher')
|
||||
})
|
||||
|
||||
it('does not have go to PDF location button nor arrow icon', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'not.exist'
|
||||
)
|
||||
|
||||
cy.get('.synctex-control-icon').should('not.exist')
|
||||
})
|
||||
|
||||
it('send set highlights action', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
// mock editor cursor position update
|
||||
cy.window().then(win => {
|
||||
win.dispatchEvent(
|
||||
new CustomEvent('cursor:editor:update', {
|
||||
detail: { row: 100, column: 10 },
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
|
||||
cy.interceptAsync({ pathname: '/project/*/sync/code' }, 'sync-code').then(
|
||||
syncCodeResponse => {
|
||||
cy.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
})
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
})
|
||||
.should('be.disabled')
|
||||
.then(() => {
|
||||
syncCodeResponse.resolve({
|
||||
body: { pdf: cloneDeep(mockHighlights) },
|
||||
})
|
||||
})
|
||||
|
||||
cy.wait('@sync-code')
|
||||
}
|
||||
)
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
}).should('not.be.disabled')
|
||||
|
||||
// synctex is called locally and the result are broadcast for the detached tab
|
||||
// NOTE: can't use `.to.deep.include({…})` as it doesn't match the nested array
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detacher',
|
||||
event: 'action-setHighlights',
|
||||
data: { args: [mockHighlights] },
|
||||
})
|
||||
})
|
||||
|
||||
it('reacts to sync to code action', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.interceptAsync({ pathname: '/project/*/sync/pdf' }, 'sync-pdf')
|
||||
.then(syncPdfResponse => {
|
||||
syncPdfResponse.resolve({
|
||||
body: { code: [{ file: 'main.tex', line: 100 }] },
|
||||
})
|
||||
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detached',
|
||||
event: 'action-sync-to-code',
|
||||
data: {
|
||||
args: [mockPosition],
|
||||
},
|
||||
})
|
||||
})
|
||||
.wait('@sync-pdf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with detached role', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-detachRole', 'detached')
|
||||
})
|
||||
|
||||
it('does not have go to code location button nor arrow icon', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile()
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: 'Go to code location in PDF',
|
||||
}).should('not.exist')
|
||||
|
||||
cy.get('.synctex-control-icon').should('not.exist')
|
||||
})
|
||||
|
||||
it('send go to code line action', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile().then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: `state-position`,
|
||||
data: { value: mockPosition },
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', {
|
||||
name: /^Go to PDF location in code/,
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
cy.spy(detachChannel, 'postMessage').as('postDetachMessage')
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).click()
|
||||
|
||||
// the button is only disabled when the state is updated
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
cy.get('.synctex-spin-icon').should('not.exist')
|
||||
|
||||
cy.get('@postDetachMessage').should('have.been.calledWith', {
|
||||
role: 'detached',
|
||||
event: 'action-sync-to-code',
|
||||
data: {
|
||||
args: [{ visualOffset: 72 }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('update inflight state', function () {
|
||||
cy.interceptCompile()
|
||||
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.waitForCompile().then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: `state-position`,
|
||||
data: { value: mockPosition },
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
cy.findByRole('status', { hidden: true }).should('not.exist')
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-sync-to-code-inflight',
|
||||
data: { value: true },
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'be.disabled'
|
||||
)
|
||||
|
||||
cy.findByRole('status', { hidden: true }).should('have.length', 1)
|
||||
|
||||
cy.wrap(null).then(() => {
|
||||
testDetachChannel.postMessage({
|
||||
role: 'detacher',
|
||||
event: 'state-sync-to-code-inflight',
|
||||
data: { value: false },
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: /^Go to PDF location in code/ }).should(
|
||||
'not.be.disabled'
|
||||
)
|
||||
|
||||
cy.get('.synctex-spin-icon').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
24
services/web/test/frontend/components/pdf-preview/scope.tsx
Normal file
24
services/web/test/frontend/components/pdf-preview/scope.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
export const mockScope = () => ({
|
||||
settings: {
|
||||
syntaxValidation: false,
|
||||
pdfViewer: 'pdfjs',
|
||||
},
|
||||
editor: {
|
||||
open_doc_name: 'main.tex',
|
||||
sharejs_doc: {
|
||||
doc_id: 'test-doc',
|
||||
getSnapshot: () => 'some doc content',
|
||||
hasBufferedOps: () => false,
|
||||
},
|
||||
view: new EditorView({
|
||||
doc: '\\documentclass{article}',
|
||||
}),
|
||||
},
|
||||
hasLintingError: false,
|
||||
ui: {
|
||||
view: 'editor',
|
||||
pdfLayout: 'sideBySide',
|
||||
},
|
||||
})
|
@@ -0,0 +1,133 @@
|
||||
import GroupInvitationNotification from '@/features/project-list/components/notifications/groups/group-invitation/group-invitation'
|
||||
import { NotificationGroupInvitation } from '../../../../../types/project/dashboard/notification'
|
||||
|
||||
type Props = {
|
||||
notification: NotificationGroupInvitation
|
||||
}
|
||||
|
||||
function GroupInvitation({ notification }: Props) {
|
||||
return (
|
||||
<div className="user-notifications">
|
||||
<ul className="list-unstyled">
|
||||
<GroupInvitationNotification notification={notification} />
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('<GroupInvitationNotification />', function () {
|
||||
const notification: NotificationGroupInvitation = {
|
||||
_id: 1,
|
||||
templateKey: 'notification_group_invitation',
|
||||
messageOpts: {
|
||||
inviterName: 'inviter@overleaf.com',
|
||||
token: '123abc',
|
||||
managedUsersEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.intercept(
|
||||
'PUT',
|
||||
`/subscription/invites/${notification.messageOpts.token}`,
|
||||
{
|
||||
statusCode: 204,
|
||||
}
|
||||
).as('acceptInvite')
|
||||
})
|
||||
|
||||
describe('user without existing personal subscription', function () {
|
||||
it('is able to join group successfully', function () {
|
||||
cy.mount(<GroupInvitation notification={notification} />)
|
||||
|
||||
cy.findByRole('alert')
|
||||
|
||||
cy.contains(
|
||||
'inviter@overleaf.com has invited you to join a group subscription on Overleaf'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Join now' }).click()
|
||||
|
||||
cy.wait('@acceptInvite')
|
||||
|
||||
cy.findByText(
|
||||
'Congratulations! You‘ve successfully joined the group subscription.'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: /close/i }).click()
|
||||
|
||||
cy.findByRole('alert').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user with existing personal subscription', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set(
|
||||
'ol-hasIndividualRecurlySubscription',
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('is able to join group successfully without cancelling personal subscription', function () {
|
||||
cy.mount(<GroupInvitation notification={notification} />)
|
||||
|
||||
cy.findByRole('alert')
|
||||
|
||||
cy.contains(
|
||||
'inviter@overleaf.com has invited you to join a group Overleaf subscription. If you join this group, you may not need your individual subscription. Would you like to cancel it?'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Not now' }).click()
|
||||
|
||||
cy.contains(
|
||||
'inviter@overleaf.com has invited you to join a group subscription on Overleaf'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Join now' }).click()
|
||||
|
||||
cy.wait('@acceptInvite')
|
||||
|
||||
cy.findByText(
|
||||
'Congratulations! You‘ve successfully joined the group subscription.'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: /close/i }).click()
|
||||
|
||||
cy.findByRole('alert').should('not.exist')
|
||||
})
|
||||
|
||||
it('is able to join group successfully after cancelling personal subscription', function () {
|
||||
cy.intercept('POST', '/user/subscription/cancel', {
|
||||
statusCode: 204,
|
||||
}).as('cancelPersonalSubscription')
|
||||
|
||||
cy.mount(<GroupInvitation notification={notification} />)
|
||||
|
||||
cy.findByRole('alert')
|
||||
|
||||
cy.contains(
|
||||
'inviter@overleaf.com has invited you to join a group Overleaf subscription. If you join this group, you may not need your individual subscription. Would you like to cancel it?'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Cancel my subscription' }).click()
|
||||
|
||||
cy.wait('@cancelPersonalSubscription')
|
||||
|
||||
cy.contains(
|
||||
'inviter@overleaf.com has invited you to join a group subscription on Overleaf'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Join now' }).click()
|
||||
|
||||
cy.wait('@acceptInvite')
|
||||
|
||||
cy.findByText(
|
||||
'Congratulations! You‘ve successfully joined the group subscription.'
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: /close/i }).click()
|
||||
|
||||
cy.findByRole('alert').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
345
services/web/test/frontend/components/shared/select.spec.tsx
Normal file
345
services/web/test/frontend/components/shared/select.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
@@ -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')
|
||||
})
|
||||
})
|
@@ -0,0 +1,164 @@
|
||||
import TokenAccessPage from '@/features/token-access/components/token-access-root'
|
||||
import { location } from '@/shared/components/location'
|
||||
|
||||
describe('<TokenAccessPage/>', function () {
|
||||
// this is a URL for a read-only token, but the process is the same for read-write tokens
|
||||
const url = '/read/123/grant'
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-postUrl', url)
|
||||
win.metaAttributesCache.set('ol-user', { email: 'test@example.com' })
|
||||
})
|
||||
})
|
||||
|
||||
it('handles a successful token access request', function () {
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
requireAccept: { projectName: 'Test Project' },
|
||||
},
|
||||
}
|
||||
).as('grantRequest')
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest').then(interception => {
|
||||
expect(interception.request.body.confirmedByUser).to.be.false
|
||||
})
|
||||
|
||||
cy.get('.link-sharing-invite-header').should(
|
||||
'have.text',
|
||||
['You’re joining', 'Test Project', 'as test@example.com'].join('')
|
||||
)
|
||||
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
redirect: '/project/123',
|
||||
},
|
||||
}
|
||||
).as('confirmedGrantRequest')
|
||||
|
||||
cy.stub(location, 'replace').as('replaceLocation')
|
||||
|
||||
cy.findByRole('button', { name: 'OK, join project' }).click()
|
||||
|
||||
cy.wait('@confirmedGrantRequest').then(interception => {
|
||||
expect(interception.request.body.confirmedByUser).to.be.true
|
||||
})
|
||||
|
||||
cy.get('@replaceLocation').should(
|
||||
'have.been.calledOnceWith',
|
||||
'/project/123'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles a project not found response', function () {
|
||||
cy.intercept({ method: 'post', url, times: 1 }, { statusCode: 404 }).as(
|
||||
'grantRequest'
|
||||
)
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest')
|
||||
|
||||
cy.get('h3').should('have.text', 'Join Project')
|
||||
cy.get('h4').should('have.text', 'Project not found')
|
||||
|
||||
cy.findByRole('button', { name: 'Join Project' }).should('not.exist')
|
||||
})
|
||||
|
||||
it('handles a redirect response', function () {
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
redirect: '/restricted',
|
||||
},
|
||||
}
|
||||
).as('grantRequest')
|
||||
|
||||
cy.stub(location, 'replace').as('replaceLocation')
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest')
|
||||
|
||||
cy.get('@replaceLocation').should('have.been.calledOnceWith', '/restricted')
|
||||
})
|
||||
|
||||
it('handles a v1 "must login" response', function () {
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
v1Import: { status: 'mustLogin' },
|
||||
},
|
||||
}
|
||||
).as('grantRequest')
|
||||
|
||||
cy.stub(location, 'replace').as('replaceLocation')
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest')
|
||||
|
||||
cy.get('h1').should('have.text', 'Please log in')
|
||||
|
||||
cy.findByRole('link', { name: 'Log in to access project' })
|
||||
.should('have.attr', 'href')
|
||||
.and('match', /^\/login\?redir=/)
|
||||
})
|
||||
|
||||
it('handles a v1 "cannot import" response', function () {
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
v1Import: { status: 'cannotImport' },
|
||||
},
|
||||
}
|
||||
).as('grantRequest')
|
||||
|
||||
cy.stub(location, 'replace').as('replaceLocation')
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest')
|
||||
|
||||
cy.get('h1').should('have.text', 'Overleaf v1 Project')
|
||||
cy.get('h2').should('have.text', 'Cannot Access Overleaf v1 Project')
|
||||
})
|
||||
|
||||
it('handles a v1 "can download zip" response', function () {
|
||||
cy.intercept(
|
||||
{ method: 'post', url, times: 1 },
|
||||
{
|
||||
body: {
|
||||
v1Import: {
|
||||
status: 'canDownloadZip',
|
||||
projectId: '123',
|
||||
name: 'Test Project',
|
||||
},
|
||||
},
|
||||
}
|
||||
).as('grantRequest')
|
||||
|
||||
cy.stub(location, 'replace').as('replaceLocation')
|
||||
|
||||
cy.mount(<TokenAccessPage />)
|
||||
|
||||
cy.wait('@grantRequest')
|
||||
|
||||
cy.get('h1').should('have.text', 'Overleaf v1 Project')
|
||||
|
||||
cy.findByRole('link', { name: 'Download project zip file' }).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'/overleaf/project/123/download/zip'
|
||||
)
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user