first commit
This commit is contained in:
8
server-ce/test/helpers/beforeWithReRunOnTestRetry.ts
Normal file
8
server-ce/test/helpers/beforeWithReRunOnTestRetry.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function beforeWithReRunOnTestRetry(fn: () => void | Promise<any>) {
|
||||
let ranOnce = false
|
||||
beforeEach(() => {
|
||||
if (ranOnce && Cypress.currentRetry === 0) return
|
||||
ranOnce = true
|
||||
return fn()
|
||||
})
|
||||
}
|
40
server-ce/test/helpers/compile.ts
Normal file
40
server-ce/test/helpers/compile.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Helper function for throttling clicks on the recompile button to avoid hitting server side rate limits.
|
||||
* The naive approach is waiting a fixed a mount of time (3s) just before clicking the button.
|
||||
* This helper takes into account that other UI interactions take time. We can deduce that latency from the fixed delay (3s minus other latency). This can bring down the effective waiting time to 0s.
|
||||
*/
|
||||
export function throttledRecompile() {
|
||||
const { queueReset, recompile } = prepareWaitForNextCompileSlot()
|
||||
queueReset()
|
||||
return recompile
|
||||
}
|
||||
|
||||
export function prepareWaitForNextCompileSlot() {
|
||||
let lastCompile = 0
|
||||
function queueReset() {
|
||||
cy.then(() => {
|
||||
lastCompile = Date.now()
|
||||
})
|
||||
}
|
||||
function waitForCompileRateLimitCoolOff(triggerCompile: () => void) {
|
||||
cy.then(() => {
|
||||
cy.log('Wait for recompile rate-limit to cool off')
|
||||
const msSinceLastCompile = Date.now() - lastCompile
|
||||
cy.wait(Math.max(0, 1_000 - msSinceLastCompile))
|
||||
queueReset()
|
||||
triggerCompile()
|
||||
cy.log('Wait for compile to finish')
|
||||
cy.findByText('Recompile').should('be.visible')
|
||||
})
|
||||
}
|
||||
function recompile() {
|
||||
waitForCompileRateLimitCoolOff(() => {
|
||||
cy.findByText('Recompile').click()
|
||||
})
|
||||
}
|
||||
return {
|
||||
queueReset,
|
||||
waitForCompileRateLimitCoolOff,
|
||||
recompile,
|
||||
}
|
||||
}
|
64
server-ce/test/helpers/config.ts
Normal file
64
server-ce/test/helpers/config.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { reconfigure } from './hostAdminClient'
|
||||
import { resetActivateUserRateLimit, resetCreatedUsersCache } from './login'
|
||||
|
||||
export const STARTUP_TIMEOUT =
|
||||
parseInt(Cypress.env('STARTUP_TIMEOUT'), 10) || 120_000
|
||||
|
||||
export function isExcludedBySharding(
|
||||
shard:
|
||||
| 'CE_DEFAULT'
|
||||
| 'CE_CUSTOM_1'
|
||||
| 'CE_CUSTOM_2'
|
||||
| 'PRO_DEFAULT_1'
|
||||
| 'PRO_DEFAULT_2'
|
||||
| 'PRO_CUSTOM_1'
|
||||
| 'PRO_CUSTOM_2'
|
||||
| 'PRO_CUSTOM_3'
|
||||
) {
|
||||
const SHARD = Cypress.env('SHARD')
|
||||
return SHARD && shard !== SHARD
|
||||
}
|
||||
|
||||
let previousConfigFrontend: string
|
||||
|
||||
export function startWith({
|
||||
pro = false,
|
||||
version = 'latest',
|
||||
vars = {},
|
||||
varsFn = () => ({}),
|
||||
withDataDir = false,
|
||||
resetData = false,
|
||||
}) {
|
||||
before(async function () {
|
||||
Object.assign(vars, varsFn())
|
||||
const cfg = JSON.stringify({
|
||||
pro,
|
||||
version,
|
||||
vars,
|
||||
withDataDir,
|
||||
resetData,
|
||||
})
|
||||
if (resetData) {
|
||||
resetCreatedUsersCache()
|
||||
resetActivateUserRateLimit()
|
||||
// no return here, always reconfigure when resetting data
|
||||
} else if (previousConfigFrontend === cfg) {
|
||||
return
|
||||
}
|
||||
|
||||
this.timeout(STARTUP_TIMEOUT)
|
||||
const { previousConfigServer } = await reconfigure({
|
||||
pro,
|
||||
version,
|
||||
vars,
|
||||
withDataDir,
|
||||
resetData,
|
||||
})
|
||||
if (previousConfigServer !== cfg) {
|
||||
await Cypress.session.clearAllSavedSessions()
|
||||
}
|
||||
previousConfigFrontend = cfg
|
||||
})
|
||||
}
|
||||
|
||||
export { reconfigure }
|
39
server-ce/test/helpers/email.ts
Normal file
39
server-ce/test/helpers/email.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Helper function for opening an email in Roundcube based mailtrap.
|
||||
* We need to cross an origin boundary, which complicates the use of variables.
|
||||
* Any variables need to be explicitly defined and the "runner" may only reference these and none from its scope.
|
||||
* It is not possible to use Cypress helper functions, e.g. from the testing library or other functions like "activateUser", inside the "runner".
|
||||
* REF: https://github.com/testing-library/cypress-testing-library/issues/221
|
||||
*/
|
||||
export function openEmail<T>(
|
||||
subject: string | RegExp,
|
||||
runner: (frame: Cypress.Chainable<JQuery<any>>, args: T) => void,
|
||||
args?: T
|
||||
) {
|
||||
const runnerS = runner.toString()
|
||||
cy.origin(
|
||||
Cypress.env('MAILTRAP_URL') || 'http://mailtrap',
|
||||
{ args: { args, runnerS, subject } },
|
||||
({ args, runnerS, subject }) => {
|
||||
cy.visit('/')
|
||||
cy.get('input[name="_user"]').type('mailtrap')
|
||||
cy.get('input[name="_pass"]').type('password-for-mailtrap')
|
||||
cy.get('button[type="submit"]').click()
|
||||
cy.url().then(url => {
|
||||
if (!url.includes('?_task=login')) return
|
||||
cy.log('mailtrap login is flaky in cypress, submit again')
|
||||
cy.get('input[name="_pass"]').type('password-for-mailtrap')
|
||||
cy.get('button[type="submit"]').click()
|
||||
})
|
||||
// Use force as the subject is partially hidden
|
||||
cy.contains(subject).click({ force: true })
|
||||
cy.log('wait for iframe loading')
|
||||
cy.wait(1000)
|
||||
cy.get('iframe[id="messagecontframe"]').then(frame => {
|
||||
// runnerS='(frame, args) => { runner body }'. Extract the runnable function.
|
||||
const runner = new Function('return ' + runnerS)()
|
||||
runner(cy.wrap(frame.prop('contentWindow').document.body), args)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
92
server-ce/test/helpers/hostAdminClient.ts
Normal file
92
server-ce/test/helpers/hostAdminClient.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
const hostAdminURL = Cypress.env('HOST_ADMIN_URL') || 'http://host-admin'
|
||||
|
||||
export async function dockerCompose(cmd: string, ...args: string[]) {
|
||||
return await fetchJSON(`${hostAdminURL}/docker/compose/${cmd}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
args,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function reconfigure({
|
||||
pro = false,
|
||||
version = 'latest',
|
||||
vars = {},
|
||||
withDataDir = false,
|
||||
resetData = false,
|
||||
}): Promise<{ previousConfigServer: string }> {
|
||||
return await fetchJSON(`${hostAdminURL}/reconfigure`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
pro,
|
||||
version,
|
||||
vars,
|
||||
withDataDir,
|
||||
resetData,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchJSON<T = { stdout: string; stderr: string }>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit
|
||||
): Promise<T> {
|
||||
if (init?.body) {
|
||||
init.headers = { 'Content-Type': 'application/json' }
|
||||
}
|
||||
let res
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
res = await fetch(input, init)
|
||||
break
|
||||
} catch {
|
||||
await sleep(3_000)
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
res = await fetch(input, init)
|
||||
}
|
||||
const { error, stdout, stderr, ...rest } = await res.json()
|
||||
if (error) {
|
||||
console.error(input, init, 'failed:', error)
|
||||
if (stdout) console.log(stdout)
|
||||
if (stderr) console.warn(stderr)
|
||||
const err = new Error(error.message)
|
||||
Object.assign(err, error)
|
||||
throw err
|
||||
}
|
||||
return { stdout, stderr, ...rest }
|
||||
}
|
||||
|
||||
export async function runScript({
|
||||
cwd,
|
||||
script,
|
||||
args = [],
|
||||
}: {
|
||||
cwd: string
|
||||
script: string
|
||||
args?: string[]
|
||||
}) {
|
||||
return await fetchJSON(`${hostAdminURL}/run/script`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
cwd,
|
||||
script,
|
||||
args,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRedisKeys() {
|
||||
const { stdout } = await fetchJSON(`${hostAdminURL}/redis/keys`, {
|
||||
method: 'GET',
|
||||
})
|
||||
return stdout.split('\n')
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
113
server-ce/test/helpers/login.ts
Normal file
113
server-ce/test/helpers/login.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { runScript } from './hostAdminClient'
|
||||
|
||||
const DEFAULT_PASSWORD = 'Passw0rd!'
|
||||
|
||||
const createdUsers = new Set<string>()
|
||||
|
||||
export function resetCreatedUsersCache() {
|
||||
createdUsers.clear()
|
||||
}
|
||||
|
||||
export async function createMongoUser({
|
||||
email,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
email: string
|
||||
isAdmin?: boolean
|
||||
}) {
|
||||
const t0 = Math.floor(Date.now() / 1000)
|
||||
const { stdout } = await runScript({
|
||||
cwd: 'services/web',
|
||||
script: 'modules/server-ce-scripts/scripts/create-user.js',
|
||||
args: [`--email=${email}`, `--admin=${isAdmin}`],
|
||||
})
|
||||
const [url] = stdout.match(/http:\/\/.+\/user\/activate\?token=\S+/)!
|
||||
const userId = new URL(url, location.origin).searchParams.get('user_id')!
|
||||
const signupDate = parseInt(userId.slice(0, 8), 16)
|
||||
if (signupDate < t0) {
|
||||
return { url, exists: true }
|
||||
}
|
||||
return { url, exists: false }
|
||||
}
|
||||
|
||||
export function ensureUserExists({
|
||||
email,
|
||||
password = DEFAULT_PASSWORD,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
email: string
|
||||
password?: string
|
||||
isAdmin?: boolean
|
||||
}) {
|
||||
let url: string
|
||||
let exists: boolean
|
||||
before(async function () {
|
||||
exists = createdUsers.has(email)
|
||||
if (exists) return
|
||||
;({ url, exists } = await createMongoUser({ email, isAdmin }))
|
||||
})
|
||||
before(function () {
|
||||
if (exists) return
|
||||
activateUser(url, password)
|
||||
cy.then(() => {
|
||||
createdUsers.add(email)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function login(username: string, password = DEFAULT_PASSWORD) {
|
||||
cy.session(
|
||||
[username, password],
|
||||
() => {
|
||||
cy.visit('/login')
|
||||
cy.get('input[name="email"]').type(username)
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.findByRole('button', { name: 'Login' }).click()
|
||||
cy.url().should('contain', '/project')
|
||||
},
|
||||
{
|
||||
cacheAcrossSpecs: true,
|
||||
async validate() {
|
||||
// Hit a cheap endpoint that is behind AuthenticationController.requireLogin().
|
||||
cy.request({ url: '/user/personal_info', followRedirect: false }).then(
|
||||
response => {
|
||||
expect(response.status).to.equal(200)
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let activateRateLimitState = { count: 0, reset: 0 }
|
||||
export function resetActivateUserRateLimit() {
|
||||
activateRateLimitState = { count: 0, reset: 0 }
|
||||
}
|
||||
|
||||
function handleActivateUserRateLimit() {
|
||||
cy.then(() => {
|
||||
activateRateLimitState.count++
|
||||
if (activateRateLimitState.reset < Date.now()) {
|
||||
activateRateLimitState.reset = Date.now() + 65_000
|
||||
activateRateLimitState.count = 1
|
||||
} else if (activateRateLimitState.count >= 6) {
|
||||
cy.wait(activateRateLimitState.reset - Date.now())
|
||||
activateRateLimitState.count = 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function activateUser(url: string, password = DEFAULT_PASSWORD) {
|
||||
handleActivateUserRateLimit()
|
||||
|
||||
cy.session(url, () => {
|
||||
cy.visit(url)
|
||||
cy.url().then(url => {
|
||||
if (url.includes('/login')) return
|
||||
cy.url().should('contain', '/user/activate')
|
||||
cy.get('input[name="password"]').type(password)
|
||||
cy.findByRole('button', { name: 'Activate' }).click()
|
||||
cy.url().should('contain', '/project')
|
||||
})
|
||||
})
|
||||
}
|
251
server-ce/test/helpers/project.ts
Normal file
251
server-ce/test/helpers/project.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { login } from './login'
|
||||
import { openEmail } from './email'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export function createProject(
|
||||
name: string,
|
||||
{
|
||||
type = 'Blank Project',
|
||||
newProjectButtonMatcher = /new project/i,
|
||||
open = true,
|
||||
}: {
|
||||
type?: 'Blank Project' | 'Example Project'
|
||||
newProjectButtonMatcher?: RegExp
|
||||
open?: boolean
|
||||
} = {}
|
||||
): Cypress.Chainable<string> {
|
||||
cy.url().then(url => {
|
||||
if (!url.endsWith('/project')) {
|
||||
cy.visit('/project')
|
||||
}
|
||||
})
|
||||
const interceptId = uuid()
|
||||
let projectId = ''
|
||||
if (!open) {
|
||||
cy.then(() => {
|
||||
// Register intercept just before creating the project, otherwise we might
|
||||
// intercept a request from a prior createProject invocation.
|
||||
cy.intercept(
|
||||
{ method: 'GET', url: /\/project\/[a-fA-F0-9]{24}$/, times: 1 },
|
||||
req => {
|
||||
projectId = req.url.split('/').pop()!
|
||||
// Redirect back to the project dashboard, effectively reload the page.
|
||||
req.redirect('/project')
|
||||
}
|
||||
).as(interceptId)
|
||||
})
|
||||
}
|
||||
cy.findAllByRole('button').contains(newProjectButtonMatcher).click()
|
||||
// FIXME: This should only look in the left menu
|
||||
cy.findAllByText(type).first().click()
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.get('input').type(name)
|
||||
cy.findByText('Create').click()
|
||||
})
|
||||
if (open) {
|
||||
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||
waitForMainDocToLoad()
|
||||
return cy
|
||||
.url()
|
||||
.should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||
.then(url => url.split('/').pop())
|
||||
} else {
|
||||
const alias = `@${interceptId}` // IDEs do not like computed values in cy.wait().
|
||||
cy.wait(alias)
|
||||
return cy.then(() => projectId)
|
||||
}
|
||||
}
|
||||
|
||||
export function openProjectByName(projectName: string) {
|
||||
cy.visit('/project')
|
||||
cy.findByText(projectName).click()
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
export function openProjectById(projectId: string) {
|
||||
cy.visit(`/project/${projectId}`)
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
export function openProjectViaLinkSharingAsAnon(url: string) {
|
||||
cy.visit(url)
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
export function openProjectViaLinkSharingAsUser(
|
||||
url: string,
|
||||
projectName: string,
|
||||
email: string
|
||||
) {
|
||||
cy.visit(url)
|
||||
cy.findByText(projectName) // wait for lazy loading
|
||||
cy.contains(`as ${email}`)
|
||||
cy.findByText('OK, join project').click()
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
export function openProjectViaInviteNotification(projectName: string) {
|
||||
cy.visit('/project')
|
||||
cy.findByText(projectName)
|
||||
.parent()
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.findByText('Join Project').click()
|
||||
})
|
||||
cy.findByText('Open Project').click()
|
||||
cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
function shareProjectByEmail(
|
||||
projectName: string,
|
||||
email: string,
|
||||
level: 'Viewer' | 'Editor'
|
||||
) {
|
||||
openProjectByName(projectName)
|
||||
cy.findByText('Share').click()
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.findByLabelText('Add people', { selector: 'input' }).type(`${email},`)
|
||||
cy.findByLabelText('Add people', { selector: 'input' })
|
||||
.parents('form')
|
||||
.within(() => {
|
||||
cy.findByTestId('add-collaborator-select')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.findByText(level).click()
|
||||
})
|
||||
})
|
||||
cy.findByText('Invite').click({ force: true })
|
||||
cy.findByText('Invite not yet accepted.')
|
||||
})
|
||||
}
|
||||
|
||||
export function shareProjectByEmailAndAcceptInviteViaDash(
|
||||
projectName: string,
|
||||
email: string,
|
||||
level: 'Viewer' | 'Editor'
|
||||
) {
|
||||
shareProjectByEmail(projectName, email, level)
|
||||
|
||||
login(email)
|
||||
openProjectViaInviteNotification(projectName)
|
||||
}
|
||||
|
||||
export function shareProjectByEmailAndAcceptInviteViaEmail(
|
||||
projectName: string,
|
||||
email: string,
|
||||
level: 'Viewer' | 'Editor'
|
||||
) {
|
||||
shareProjectByEmail(projectName, email, level)
|
||||
|
||||
login(email)
|
||||
|
||||
openEmail(projectName, frame => {
|
||||
frame.contains('View project').then(a => {
|
||||
cy.log(
|
||||
'bypass target=_blank and navigate current browser tab/cypress-iframe to project invite'
|
||||
)
|
||||
cy.visit(a.attr('href')!)
|
||||
})
|
||||
})
|
||||
cy.url().should('match', /\/project\/[a-f0-9]+\/invite\/token\/[a-f0-9]+/)
|
||||
cy.findByText(/user would like you to join/)
|
||||
cy.contains(new RegExp(`You are accepting this invite as ${email}`))
|
||||
cy.findByText('Join Project').click()
|
||||
waitForMainDocToLoad()
|
||||
}
|
||||
|
||||
export function enableLinkSharing() {
|
||||
let linkSharingReadOnly: string
|
||||
let linkSharingReadAndWrite: string
|
||||
const origin = new URL(Cypress.config().baseUrl!).origin
|
||||
|
||||
waitForMainDocToLoad()
|
||||
|
||||
cy.findByText('Share').click()
|
||||
cy.findByText('Turn on link sharing').click()
|
||||
cy.findByText('Anyone with this link can view this project')
|
||||
.next()
|
||||
.should('contain.text', origin + '/read')
|
||||
.then(el => {
|
||||
linkSharingReadOnly = el.text()
|
||||
})
|
||||
cy.findByText('Anyone with this link can edit this project')
|
||||
.next()
|
||||
.should('contain.text', origin + '/')
|
||||
.then(el => {
|
||||
linkSharingReadAndWrite = el.text()
|
||||
})
|
||||
|
||||
return cy.then(() => {
|
||||
return { linkSharingReadOnly, linkSharingReadAndWrite }
|
||||
})
|
||||
}
|
||||
|
||||
export function waitForMainDocToLoad() {
|
||||
cy.log('Wait for main doc to load; it will steal the focus after loading')
|
||||
cy.get('.cm-content').should('contain.text', 'Introduction')
|
||||
}
|
||||
|
||||
export function openFile(fileName: string, waitFor: string) {
|
||||
// force: The file-tree pane is too narrow to display the full name.
|
||||
cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
|
||||
|
||||
// wait until we've switched to the selected file
|
||||
cy.findByText('Loading…').should('not.exist')
|
||||
cy.findByText(waitFor)
|
||||
}
|
||||
|
||||
export function createNewFile() {
|
||||
const fileName = `${uuid()}.tex`
|
||||
|
||||
cy.log('create new project file')
|
||||
cy.get('button').contains('New file').click({ force: true })
|
||||
cy.findByRole('dialog').within(() => {
|
||||
cy.get('input').clear()
|
||||
cy.get('input').type(fileName)
|
||||
cy.findByText('Create').click()
|
||||
})
|
||||
// force: The file-tree pane is too narrow to display the full name.
|
||||
cy.findByTestId('file-tree').findByText(fileName).click({ force: true })
|
||||
|
||||
// wait until we've switched to the newly created empty file
|
||||
cy.findByText('Loading…').should('not.exist')
|
||||
cy.get('.cm-line').should('have.length', 1)
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
export function toggleTrackChanges(state: boolean) {
|
||||
cy.findByText('Review').click()
|
||||
cy.get('.track-changes-menu-button').then(el => {
|
||||
// when the menu is expanded renders the `expand_more` icon,
|
||||
// and the `chevron_right` icon when it's collapsed
|
||||
if (!el.text().includes('expand_more')) {
|
||||
el.click()
|
||||
}
|
||||
})
|
||||
|
||||
cy.findByText('Everyone')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('.form-check-input').then(el => {
|
||||
if (el.prop('checked') === state) return
|
||||
|
||||
const id = uuid()
|
||||
const alias = `@${id}`
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '**/track_changes',
|
||||
times: 1,
|
||||
}).as(id)
|
||||
if (state) {
|
||||
cy.get('.form-check-input').check()
|
||||
} else {
|
||||
cy.get('.form-check-input').uncheck()
|
||||
}
|
||||
cy.wait(alias)
|
||||
})
|
||||
})
|
||||
cy.contains('.toolbar-item', 'Review').click()
|
||||
}
|
52
server-ce/test/helpers/read-file.ts
Normal file
52
server-ce/test/helpers/read-file.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import pdf from 'pdf-parse'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const sleep = promisify(setTimeout)
|
||||
|
||||
const MAX_ATTEMPTS = 15
|
||||
const POLL_INTERVAL = 500
|
||||
|
||||
type ReadFileInZipArgs = {
|
||||
pathToZip: string
|
||||
fileToRead: string
|
||||
}
|
||||
|
||||
export async function readFileInZip({
|
||||
pathToZip,
|
||||
fileToRead,
|
||||
}: ReadFileInZipArgs) {
|
||||
let attempt = 0
|
||||
while (attempt < MAX_ATTEMPTS) {
|
||||
if (fs.existsSync(pathToZip)) {
|
||||
const zip = new AdmZip(path.resolve(pathToZip))
|
||||
const entry = zip
|
||||
.getEntries()
|
||||
.find(entry => entry.entryName == fileToRead)
|
||||
if (entry) {
|
||||
return entry.getData().toString('utf8')
|
||||
} else {
|
||||
throw new Error(`${fileToRead} not found in ${pathToZip}`)
|
||||
}
|
||||
}
|
||||
await sleep(POLL_INTERVAL)
|
||||
attempt++
|
||||
}
|
||||
throw new Error(`${pathToZip} not found`)
|
||||
}
|
||||
|
||||
export async function readPdf(file: string) {
|
||||
let attempt = 0
|
||||
while (attempt < MAX_ATTEMPTS) {
|
||||
if (fs.existsSync(file)) {
|
||||
const dataBuffer = fs.readFileSync(path.resolve(file))
|
||||
const { text } = await pdf(dataBuffer)
|
||||
return text
|
||||
}
|
||||
await sleep(POLL_INTERVAL)
|
||||
attempt++
|
||||
}
|
||||
throw new Error(`${file} not found`)
|
||||
}
|
39
server-ce/test/helpers/waitUntilScrollingFinished.ts
Normal file
39
server-ce/test/helpers/waitUntilScrollingFinished.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export function waitUntilScrollingFinished(selector: string, start = -1) {
|
||||
const pollSlow = 100
|
||||
const pollFast = 10
|
||||
const deadline =
|
||||
performance.now() + Cypress.config('defaultCommandTimeout') - pollSlow * 2
|
||||
return cy.get(selector).then(el => {
|
||||
cy.log(
|
||||
`waiting until scrolling finished for ${selector}, starting from ${start}`
|
||||
)
|
||||
return new Cypress.Promise<number>((resolve, reject) => {
|
||||
const waitForStable = (prev: number, stableFor: number) => {
|
||||
if (performance.now() > deadline) {
|
||||
return reject(new Error('timeout waiting for scrolling to finish'))
|
||||
}
|
||||
const current = el.scrollTop()!
|
||||
if (current !== prev) {
|
||||
setTimeout(() => waitForStable(current, 0), pollFast)
|
||||
} else if (stableFor < 5) {
|
||||
setTimeout(() => waitForStable(current, stableFor + 1), pollFast)
|
||||
} else {
|
||||
resolve(current)
|
||||
}
|
||||
}
|
||||
|
||||
const waitForChange = () => {
|
||||
if (performance.now() > deadline) {
|
||||
return reject(new Error('timeout waiting for scrolling to start'))
|
||||
}
|
||||
const current = el.scrollTop()!
|
||||
if (current === start) {
|
||||
setTimeout(() => waitForChange(), pollSlow)
|
||||
} else {
|
||||
setTimeout(() => waitForStable(current, 0), pollFast)
|
||||
}
|
||||
}
|
||||
waitForChange()
|
||||
})
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user