first commit

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

View File

@@ -0,0 +1,243 @@
import { v4 as uuid } from 'uuid'
const outputFiles = () => {
const build = uuid()
return [
{
path: 'output.pdf',
build,
url: `/build/${build}/output.pdf`,
type: 'pdf',
},
{
path: 'output.bbl',
build,
url: `/build/${build}/output.bbl`,
type: 'bbl',
},
{
path: 'output.bib',
build,
url: `/build/${build}/output.bib`,
type: 'bib',
},
{
path: 'example.txt',
build,
url: `/build/${build}/example.txt`,
type: 'txt',
},
{
path: 'output.log',
build,
url: `/build/${build}/output.log`,
type: 'log',
},
{
path: 'output.blg',
build,
url: `/build/${build}/output.blg`,
type: 'blg',
},
]
}
const compileFromCacheResponse = () => {
return {
fromCache: true,
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: outputFiles(),
options: {
rootResourcePath: 'main.tex',
imageName: 'texlive-full:2024.1',
compiler: 'pdflatex',
stopOnFirstError: false,
draft: false,
},
}
}
export const interceptCompileFromCacheRequest = ({
times,
promise,
}: {
times: number
promise: Promise<void>
}) => {
return cy.intercept(
{ path: '/project/*/output/cached/output.overleaf.json', times },
async req => {
await promise
req.reply({ body: compileFromCacheResponse() })
}
)
}
export const interceptCompileRequest = ({ times = 1 } = {}) => {
return cy.intercept(
{ method: 'POST', pathname: '/project/*/compile', times },
{
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: outputFiles(),
},
}
)
}
export const interceptCompile = ({
prefix = 'compile',
times = 1,
cached = false,
regular = true,
outputPDFFixture = 'output.pdf',
} = {}) => {
if (cached) {
cy.intercept(
{ path: '/project/*/output/cached/output.overleaf.json', times },
{ body: compileFromCacheResponse() }
).as(`${prefix}-cached`)
} else {
cy.intercept(
{ pathname: '/project/*/output/cached/output.overleaf.json', times },
{ statusCode: 404 }
).as(`${prefix}-cached`)
}
if (regular) {
interceptCompileRequest({ times }).as(`${prefix}`)
} else {
cy.intercept(
{ method: 'POST', pathname: '/project/*/compile', times },
{
body: {
status: 'unavailable',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [],
},
}
).as(`${prefix}`)
}
cy.intercept(
{ pathname: '/build/*/output.pdf', times },
{ fixture: `build/${outputPDFFixture},null` }
).as(`${prefix}-pdf`)
cy.intercept(
{ pathname: '/build/*/output.log', times },
{ fixture: 'build/output.log' }
).as(`${prefix}-log`)
cy.intercept(
{ pathname: '/build/*/output.blg', times },
{ fixture: 'build/output.blg' }
).as(`${prefix}-blg`)
}
export const waitForCompile = ({
prefix = 'compile',
pdf = false,
cached = false,
regular = true,
} = {}) => {
if (cached) {
cy.wait(`@${prefix}-cached`)
}
if (regular) {
cy.wait(`@${prefix}`)
}
return waitForCompileOutput({ prefix, pdf, cached })
}
export const waitForCompileOutput = ({
prefix = 'compile',
pdf = false,
cached = false,
} = {}) => {
cy.wait(`@${prefix}-log`)
.its('request.query.clsiserverid')
.should('eq', cached ? 'cache' : 'foo') // straight from cache if cached
cy.wait(`@${prefix}-blg`)
.its('request.query.clsiserverid')
.should('eq', cached ? 'cache' : 'foo') // straight from cache if cached
if (pdf) {
cy.wait(`@${prefix}-pdf`)
.its('request.query.clsiserverid')
.should('eq', 'foo') // always from VM first
}
return cy.wrap(null)
}
export const interceptDeferredCompile = (beforeResponse?: () => void) => {
const { promise, resolve } = Promise.withResolvers<void>()
cy.intercept(
{ method: 'POST', url: '/project/*/compile*', times: 1 },
req => {
if (beforeResponse) {
beforeResponse()
}
// only reply once the Promise is resolved
promise.then(() => {
req.reply({
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [
{
path: 'output.pdf',
build: '123',
url: '/build/123/output.pdf',
type: 'pdf',
},
{
path: 'output.log',
build: '123',
url: '/build/123/output.log',
type: 'log',
},
{
path: 'output.blg',
build: '123',
url: '/build/123/output.blg',
type: 'log',
},
],
},
})
})
return promise
}
).as('compile')
cy.intercept(
{ pathname: '/build/*/output.pdf', times: 1 },
{ fixture: 'build/output.pdf,null' }
).as(`compile-pdf`)
cy.intercept(
{ pathname: '/build/*/output.log', times: 1 },
{ fixture: 'build/output.log' }
).as(`compile-log`)
cy.intercept(
{ pathname: '/build/*/output.blg', times: 1 },
{ fixture: 'build/output.blg' }
).as(`compile-blg`)
return cy.wrap(resolve)
}

View File

@@ -0,0 +1,5 @@
export const interceptEvents = () => {
cy.intercept('POST', '/event/*', {
statusCode: 204,
}).as('createEvent')
}

View File

@@ -0,0 +1,56 @@
import '@testing-library/cypress/add-commands'
import {
interceptCompile,
interceptCompileFromCacheRequest,
waitForCompile,
interceptDeferredCompile,
interceptCompileRequest,
} from './compile'
import { interceptEvents } from './events'
import { interceptAsync } from './intercept-async'
import { interceptFileUpload } from './upload'
import { interceptProjectListing } from './project-list'
import { interceptLinkedFile } from './linked-file'
import { interceptMathJax } from './mathjax'
import { interceptMetadata } from './metadata'
import { interceptTutorials } from './tutorials'
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-namespace
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace,no-unused-vars
namespace Cypress {
// eslint-disable-next-line no-unused-vars
interface Chainable {
interceptAsync: typeof interceptAsync
interceptCompile: typeof interceptCompile
interceptCompileRequest: typeof interceptCompileRequest
interceptCompileFromCacheRequest: typeof interceptCompileFromCacheRequest
interceptEvents: typeof interceptEvents
interceptMetadata: typeof interceptMetadata
waitForCompile: typeof waitForCompile
interceptDeferredCompile: typeof interceptDeferredCompile
interceptFileUpload: typeof interceptFileUpload
interceptProjectListing: typeof interceptProjectListing
interceptLinkedFile: typeof interceptLinkedFile
interceptMathJax: typeof interceptMathJax
interceptTutorials: typeof interceptTutorials
}
}
}
Cypress.Commands.add('interceptAsync', interceptAsync)
Cypress.Commands.add('interceptCompile', interceptCompile)
Cypress.Commands.add('interceptCompileRequest', interceptCompileRequest)
Cypress.Commands.add(
'interceptCompileFromCacheRequest',
interceptCompileFromCacheRequest
)
Cypress.Commands.add('interceptEvents', interceptEvents)
Cypress.Commands.add('interceptMetadata', interceptMetadata)
Cypress.Commands.add('waitForCompile', waitForCompile)
Cypress.Commands.add('interceptDeferredCompile', interceptDeferredCompile)
Cypress.Commands.add('interceptFileUpload', interceptFileUpload)
Cypress.Commands.add('interceptProjectListing', interceptProjectListing)
Cypress.Commands.add('interceptLinkedFile', interceptLinkedFile)
Cypress.Commands.add('interceptMathJax', interceptMathJax)
Cypress.Commands.add('interceptTutorials', interceptTutorials)

View File

@@ -0,0 +1,19 @@
import { RouteHandler, RouteMatcher } from 'cypress/types/net-stubbing'
export const interceptAsync = (route: RouteMatcher, alias: string) => {
const deferred: { resolve: (value: RouteHandler) => void } = {
resolve: () => {
console.error('This should never be called')
},
}
const promise = new Promise<RouteHandler>(resolve => {
deferred.resolve = resolve
})
cy.intercept(route, req => {
return promise.then(response => req.reply(response))
}).as(alias)
return cy.wrap(deferred)
}

View File

@@ -0,0 +1,12 @@
import { HttpRequestInterceptor } from 'cypress/types/net-stubbing'
export const interceptLinkedFile = () => {
cy.intercept(
{ method: 'POST', url: '/project/*/linked_file' },
cy
.spy((req: Parameters<HttpRequestInterceptor>[0]) => {
req.reply({ statusCode: 200, body: { success: true } })
})
.as('linked-file-request')
)
}

View File

@@ -0,0 +1,32 @@
const MATHJAX_STUB = `
window.MathJax = {
startup: {
promise: Promise.resolve()
},
svgStylesheet: () => document.createElement("STYLE"),
typesetPromise: (elements) => {
for (const element of elements) {
// This will keep math delimeters around the text
element.classList.add('MathJax')
}
return Promise.resolve()
},
tex2svgPromise: (content) => {
const text = document.createElement('SPAN')
text.classList.add('MathJax')
text.innerText = content
return Promise.resolve(text)
},
getMetricsFor: () => ({}),
texReset: () => {},
}
`
export const interceptMathJax = () => {
// NOTE: this is just a URL to be intercepted with the stub, not the real (versioned) MathJax URL
const url = '/js/libs/mathjax/es5/tex-svg-full.js'
cy.window().then(win => {
win.metaAttributesCache.set('ol-mathJaxPath', url)
})
cy.intercept('GET', url, MATHJAX_STUB).as('mathjax-load-request')
}

View File

@@ -0,0 +1,3 @@
export const interceptMetadata = () => {
cy.intercept('POST', '/project/*/doc/*/metadata', {})
}

View File

@@ -0,0 +1,22 @@
export const interceptProjectListing = () => {
cy.intercept('GET', '/user/projects', {
projects: [
{
_id: 'fake-project-1',
accessLevel: 'owner',
name: 'My first project',
},
{
_id: 'fake-project-2',
accessLevel: 'owner',
name: 'My second project',
},
],
})
cy.intercept('GET', '/project/*/entities', {
entities: [
{ path: '/frog.jpg', type: 'file' },
{ path: 'figures/unicorn.png', type: 'file' },
],
})
}

View File

@@ -0,0 +1,5 @@
export const interceptTutorials = () => {
cy.intercept('POST', '/tutorial/**', {
statusCode: 204,
}).as('completeTutorial')
}

View File

@@ -0,0 +1,18 @@
import { HttpRequestInterceptor } from 'cypress/types/net-stubbing'
export const interceptFileUpload = () => {
cy.intercept(
{ method: 'POST', url: /\/project\/.*\/upload/ },
cy
.spy((req: Parameters<HttpRequestInterceptor>[0]) => {
const folderMatch = req.url.match(
/project\/.*\/upload\?folder_id=[a-f0-9]{24}/
)
if (!folderMatch) {
req.reply({ statusCode: 500, body: { success: false } })
}
req.reply({ statusCode: 200, body: { success: true } })
})
.as('uploadRequest')
)
}

View File

@@ -0,0 +1,6 @@
Cypress.on('uncaught:exception', err => {
// don't fail the test for ResizeObserver error messages
if (err.message.includes('ResizeObserver')) {
return false
}
})