first commit
This commit is contained in:
208
services/web/test/frontend/infrastructure/fetch-json.test.js
Normal file
208
services/web/test/frontend/infrastructure/fetch-json.test.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { expect } from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {
|
||||
deleteJSON,
|
||||
FetchError,
|
||||
getUserFacingMessage,
|
||||
getJSON,
|
||||
postJSON,
|
||||
putJSON,
|
||||
} from '../../../frontend/js/infrastructure/fetch-json'
|
||||
|
||||
describe('fetchJSON', function () {
|
||||
before(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
it('handles GET requests', function () {
|
||||
fetchMock.once(
|
||||
{ method: 'GET', url: '/test', headers },
|
||||
{ status: 200, body: { result: 'success' } }
|
||||
)
|
||||
|
||||
return expect(getJSON('/test')).to.eventually.deep.equal({
|
||||
result: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles 4xx responses', function () {
|
||||
fetchMock.get('/test', {
|
||||
status: 400,
|
||||
body: { message: 'The request was invalid' },
|
||||
})
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Bad Request')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
message: 'Bad Request',
|
||||
'data.message': 'The request was invalid',
|
||||
'response.status': 400,
|
||||
'info.statusCode': 400,
|
||||
})
|
||||
})
|
||||
|
||||
it('handles 5xx responses', async function () {
|
||||
fetchMock.get('/test', { status: 500 })
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Internal Server Error')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
'response.status': 500,
|
||||
'info.statusCode': 500,
|
||||
})
|
||||
})
|
||||
|
||||
it('handles JSON error responses', async function () {
|
||||
fetchMock.get('/test', {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: { message: 'lorem ipsum' },
|
||||
})
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Internal Server Error')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
'data.message': 'lorem ipsum',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles text error responses', async function () {
|
||||
fetchMock.get('/test', {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: 'lorem ipsum',
|
||||
})
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Internal Server Error')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
'data.message': 'lorem ipsum',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles text error responses sent as HTML', async function () {
|
||||
fetchMock.get('/test', {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
},
|
||||
body: 'lorem ipsum',
|
||||
})
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Internal Server Error')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
'data.message': 'lorem ipsum',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles (ignores) HTML error responses sent as HTML', async function () {
|
||||
fetchMock.get('/test', {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
},
|
||||
body: '<!doctype html><html lang="en"><body><p>lorem ipsum</p></body></html>',
|
||||
})
|
||||
|
||||
const promise = getJSON('/test')
|
||||
|
||||
expect(promise)
|
||||
.to.eventually.be.rejectedWith('Internal Server Error')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
|
||||
try {
|
||||
await promise
|
||||
} catch (error) {
|
||||
expect(error.data).to.eql({})
|
||||
}
|
||||
})
|
||||
|
||||
it('handles 5xx responses without a status message', async function () {
|
||||
fetchMock.get('/test', { status: 599 })
|
||||
|
||||
return expect(getJSON('/test'))
|
||||
.to.eventually.be.rejectedWith('Unexpected Error: 599')
|
||||
.and.be.an.instanceOf(FetchError)
|
||||
.to.nested.include({
|
||||
'response.status': 599,
|
||||
'info.statusCode': 599,
|
||||
message: 'Unexpected Error: 599',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles POST requests', function () {
|
||||
const body = { example: true }
|
||||
|
||||
fetchMock.once(
|
||||
{ method: 'POST', url: '/test', headers, body },
|
||||
{ status: 200, body: { result: 'success' } }
|
||||
)
|
||||
|
||||
return expect(postJSON('/test', { body })).to.eventually.deep.equal({
|
||||
result: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles PUT requests', function () {
|
||||
const body = { example: true }
|
||||
|
||||
fetchMock.once(
|
||||
{ method: 'PUT', url: '/test', headers, body },
|
||||
{ status: 200, body: { result: 'success' } }
|
||||
)
|
||||
|
||||
return expect(putJSON('/test', { body })).to.eventually.deep.equal({
|
||||
result: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('handles DELETE requests', function () {
|
||||
fetchMock.once({ method: 'DELETE', url: '/test', headers }, { status: 204 })
|
||||
|
||||
return expect(deleteJSON('/test')).to.eventually.deep.equal({})
|
||||
})
|
||||
|
||||
describe('getUserFacingMessage()', function () {
|
||||
it('returns the error facing message for FetchError instances', function () {
|
||||
const error = new FetchError(
|
||||
'403 error',
|
||||
'http:/example.com',
|
||||
{},
|
||||
{ status: 403 }
|
||||
)
|
||||
expect(getUserFacingMessage(error)).to.equal(
|
||||
'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns `message` for Error instances different than FetchError', function () {
|
||||
const error = new Error('403 error')
|
||||
expect(getUserFacingMessage(error)).to.equal('403 error')
|
||||
})
|
||||
|
||||
it('returns `undefined` for non-Error instances', function () {
|
||||
expect(getUserFacingMessage(undefined)).to.be.undefined
|
||||
expect(getUserFacingMessage(null)).to.be.undefined
|
||||
expect(getUserFacingMessage('error')).to.be.undefined
|
||||
})
|
||||
})
|
||||
})
|
147
services/web/test/frontend/infrastructure/i18n.spec.tsx
Normal file
147
services/web/test/frontend/infrastructure/i18n.spec.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
describe('i18n', function () {
|
||||
describe('t', function () {
|
||||
it('translates a plain string', function () {
|
||||
const Test = () => {
|
||||
const { t } = useTranslation()
|
||||
return <div>{t('accept_change')}</div>
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByText('Accept change')
|
||||
})
|
||||
|
||||
it('uses defaultValues', function () {
|
||||
const Test = () => {
|
||||
const { t } = useTranslation()
|
||||
return <div>{t('welcome_to_sl')}</div>
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByText('Welcome to Overleaf')
|
||||
})
|
||||
|
||||
it('uses values', function () {
|
||||
const Test = () => {
|
||||
const { t } = useTranslation()
|
||||
return <div>{t('sort_by_x', { x: 'name' })}</div>
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByText('Sort by name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trans', function () {
|
||||
it('uses values', function () {
|
||||
const Test = () => {
|
||||
return (
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="sort_by_x"
|
||||
values={{ x: 'name' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByText('Sort by name')
|
||||
})
|
||||
|
||||
it('uses an object of components', function () {
|
||||
const Test = () => {
|
||||
return (
|
||||
<div data-testid="container">
|
||||
<Trans
|
||||
i18nKey="in_order_to_match_institutional_metadata_associated"
|
||||
components={{ b: <b /> }}
|
||||
values={{ email: 'test@example.com' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByTestId('container')
|
||||
.should(
|
||||
'have.text',
|
||||
'In order to match your institutional metadata, your account is associated with the email test@example.com.'
|
||||
)
|
||||
.find('b')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', 'test@example.com')
|
||||
})
|
||||
|
||||
it('uses an array of components', function () {
|
||||
const Test = () => {
|
||||
return (
|
||||
<div data-testid="container">
|
||||
<Trans
|
||||
i18nKey="are_you_still_at"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: 'Test' }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByTestId('container')
|
||||
.should('have.text', 'Are you still at Test?')
|
||||
.find('b')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', 'Test')
|
||||
})
|
||||
|
||||
it('escapes special characters', function () {
|
||||
const Test = () => {
|
||||
return (
|
||||
<div data-testid="container">
|
||||
<Trans
|
||||
i18nKey="are_you_still_at"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: "T&e's<code>t</code>ing" }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByTestId('container')
|
||||
.should('have.text', "Are you still at T&e's<code>t</code>ing?")
|
||||
.find('b')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', "T&e's<code>t</code>ing")
|
||||
})
|
||||
|
||||
it('does not convert markup in values to components', function () {
|
||||
const Test = () => {
|
||||
return (
|
||||
<div data-testid="container">
|
||||
<Trans
|
||||
i18nKey="are_you_still_at"
|
||||
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||
values={{
|
||||
institutionName: "<i>T</i>&<b>e</b>'s<code>t</code>ing",
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
cy.mount(<Test />)
|
||||
cy.findByTestId('container')
|
||||
.should(
|
||||
'have.text',
|
||||
"Are you still at <i>T</i>&<b>e</b>'s<code>t</code>ing?"
|
||||
)
|
||||
.find('b')
|
||||
.should('have.length', 1)
|
||||
.should('have.text', "<i>T</i>&<b>e</b>'s<code>t</code>ing")
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,88 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
describe('localStorage', function () {
|
||||
let originalLocalStorage
|
||||
before(function () {
|
||||
originalLocalStorage = global.localStorage
|
||||
})
|
||||
|
||||
after(function () {
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: originalLocalStorage,
|
||||
})
|
||||
})
|
||||
|
||||
let spyOnDebugConsoleError
|
||||
beforeEach(function () {
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: {
|
||||
getItem: sinon.stub().returns(null),
|
||||
setItem: sinon.stub(),
|
||||
clear: sinon.stub(),
|
||||
removeItem: sinon.stub(),
|
||||
},
|
||||
})
|
||||
|
||||
spyOnDebugConsoleError = sinon.spy(debugConsole, 'error')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
spyOnDebugConsoleError.restore()
|
||||
Object.defineProperty(global, 'localStorage', { value: undefined })
|
||||
})
|
||||
|
||||
it('getItem', function () {
|
||||
expect(customLocalStorage.getItem('foo')).to.be.null
|
||||
|
||||
global.localStorage.getItem.returns('false')
|
||||
expect(customLocalStorage.getItem('foo')).to.equal(false)
|
||||
|
||||
global.localStorage.getItem.returns('{"foo":"bar"}')
|
||||
expect(customLocalStorage.getItem('foo')).to.deep.equal({ foo: 'bar' })
|
||||
|
||||
global.localStorage.getItem.throws(new Error('Nope'))
|
||||
expect(customLocalStorage.getItem('foo')).to.be.null
|
||||
expect(debugConsole.error).to.be.calledOnce
|
||||
})
|
||||
|
||||
it('setItem', function () {
|
||||
customLocalStorage.setItem('foo', 'bar')
|
||||
expect(global.localStorage.setItem).to.be.calledOnceWith('foo', '"bar"')
|
||||
global.localStorage.setItem.reset()
|
||||
|
||||
customLocalStorage.setItem('foo', true)
|
||||
expect(global.localStorage.setItem).to.be.calledOnceWith('foo', 'true')
|
||||
global.localStorage.setItem.reset()
|
||||
|
||||
customLocalStorage.setItem('foo', { bar: 1 })
|
||||
expect(global.localStorage.setItem).to.be.calledOnceWith('foo', '{"bar":1}')
|
||||
global.localStorage.setItem.reset()
|
||||
|
||||
global.localStorage.setItem.throws(new Error('Nope'))
|
||||
expect(customLocalStorage.setItem('foo', 'bar')).to.be.null
|
||||
expect(debugConsole.error).to.be.calledOnce
|
||||
})
|
||||
|
||||
it('clear', function () {
|
||||
customLocalStorage.clear()
|
||||
expect(global.localStorage.clear).to.be.calledOnce
|
||||
|
||||
global.localStorage.clear.throws(new Error('Nope'))
|
||||
expect(customLocalStorage.clear()).to.be.null
|
||||
expect(debugConsole.error).to.be.calledOnce
|
||||
})
|
||||
|
||||
it('removeItem', function () {
|
||||
customLocalStorage.removeItem('foo')
|
||||
expect(global.localStorage.removeItem).to.be.calledOnceWith('foo')
|
||||
global.localStorage.removeItem.reset()
|
||||
|
||||
global.localStorage.removeItem.throws(new Error('Nope'))
|
||||
expect(customLocalStorage.removeItem('foo')).to.be.null
|
||||
expect(debugConsole.error).to.be.calledOnce
|
||||
})
|
||||
})
|
@@ -0,0 +1,387 @@
|
||||
import { expect } from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
|
||||
|
||||
describe('ProjectSnapshot', function () {
|
||||
let snapshot: ProjectSnapshot
|
||||
const projectId = 'project-id'
|
||||
|
||||
beforeEach(function () {
|
||||
snapshot = new ProjectSnapshot(projectId)
|
||||
})
|
||||
|
||||
describe('before initialization', function () {
|
||||
describe('getDocPaths()', function () {
|
||||
it('returns an empty string', function () {
|
||||
expect(snapshot.getDocPaths()).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDocContents()', function () {
|
||||
it('returns null', function () {
|
||||
expect(snapshot.getDocContents('main.tex')).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const files = {
|
||||
'main.tex': {
|
||||
contents: '\\documentclass{article}\netc.',
|
||||
hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
},
|
||||
'hello.txt': {
|
||||
contents: 'Hello history!',
|
||||
hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
},
|
||||
'goodbye.txt': {
|
||||
contents: "We're done here",
|
||||
hash: 'dddddddddddddddddddddddddddddddddddddddd',
|
||||
},
|
||||
}
|
||||
|
||||
const chunk = {
|
||||
history: {
|
||||
snapshot: {
|
||||
files: {},
|
||||
},
|
||||
changes: [
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
pathname: 'hello.txt',
|
||||
file: {
|
||||
hash: files['hello.txt'].hash,
|
||||
stringLength: files['hello.txt'].contents.length,
|
||||
},
|
||||
},
|
||||
{
|
||||
pathname: 'main.tex',
|
||||
file: {
|
||||
hash: files['main.tex'].hash,
|
||||
stringLength: files['main.tex'].contents.length,
|
||||
},
|
||||
},
|
||||
{
|
||||
pathname: 'frog.jpg',
|
||||
file: {
|
||||
hash: 'cccccccccccccccccccccccccccccccccccccccc',
|
||||
byteLength: 97080,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: '2025-01-01T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
startVersion: 0,
|
||||
}
|
||||
|
||||
const changes = [
|
||||
{
|
||||
operations: [
|
||||
{
|
||||
pathname: 'hello.txt',
|
||||
textOperation: ['Quote: ', files['hello.txt'].contents.length],
|
||||
},
|
||||
{
|
||||
pathname: 'goodbye.txt',
|
||||
file: {
|
||||
hash: files['goodbye.txt'].hash,
|
||||
stringLength: files['goodbye.txt'].contents.length,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: '2025-01-01T13:00:00.000Z',
|
||||
},
|
||||
]
|
||||
|
||||
function mockFlush(
|
||||
opts: { repeat?: number; failOnCall?: (call: number) => boolean } = {}
|
||||
) {
|
||||
let currentCall = 0
|
||||
const getResponse = () => {
|
||||
currentCall += 1
|
||||
return opts.failOnCall?.(currentCall) ? 500 : 200
|
||||
}
|
||||
|
||||
fetchMock.post(`/project/${projectId}/flush`, getResponse, {
|
||||
name: 'flush',
|
||||
repeat: opts.repeat ?? 1,
|
||||
})
|
||||
}
|
||||
|
||||
function mockLatestChunk() {
|
||||
fetchMock.getOnce(
|
||||
`/project/${projectId}/latest/history`,
|
||||
{ chunk },
|
||||
{ name: 'latest-chunk' }
|
||||
)
|
||||
}
|
||||
|
||||
function mockChanges() {
|
||||
fetchMock.getOnce(`/project/${projectId}/changes?since=1`, changes, {
|
||||
name: 'changes-1',
|
||||
})
|
||||
fetchMock.get(`/project/${projectId}/changes?since=2`, [], {
|
||||
name: 'changes-2',
|
||||
})
|
||||
}
|
||||
|
||||
function mockBlobs(paths = Object.keys(files) as (keyof typeof files)[]) {
|
||||
for (const path of paths) {
|
||||
const file = files[path]
|
||||
fetchMock.get(`/project/${projectId}/blob/${file.hash}`, file.contents)
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeSnapshot() {
|
||||
mockFlush()
|
||||
mockLatestChunk()
|
||||
mockBlobs(['main.tex', 'hello.txt'])
|
||||
await snapshot.refresh()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
}
|
||||
|
||||
describe('after initialization', function () {
|
||||
beforeEach(initializeSnapshot)
|
||||
|
||||
describe('getDocPaths()', function () {
|
||||
it('returns the editable docs', function () {
|
||||
expect(snapshot.getDocPaths()).to.have.members([
|
||||
'main.tex',
|
||||
'hello.txt',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDocContents()', function () {
|
||||
it('returns the doc contents', function () {
|
||||
expect(snapshot.getDocContents('main.tex')).to.equal(
|
||||
files['main.tex'].contents
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null for binary files', function () {
|
||||
expect(snapshot.getDocContents('frog.jpg')).to.be.null
|
||||
})
|
||||
|
||||
it('returns null for inexistent files', function () {
|
||||
expect(snapshot.getDocContents('does-not-exist.txt')).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function refreshSnapshot() {
|
||||
mockFlush()
|
||||
mockChanges()
|
||||
mockBlobs(['goodbye.txt'])
|
||||
await snapshot.refresh()
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
}
|
||||
|
||||
describe('after refresh', function () {
|
||||
beforeEach(initializeSnapshot)
|
||||
beforeEach(refreshSnapshot)
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
describe('getDocPaths()', function () {
|
||||
it('returns the editable docs', function () {
|
||||
expect(snapshot.getDocPaths()).to.have.members([
|
||||
'main.tex',
|
||||
'hello.txt',
|
||||
'goodbye.txt',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDocContents()', function () {
|
||||
it('returns the up to date content', function () {
|
||||
expect(snapshot.getDocContents('hello.txt')).to.equal(
|
||||
`Quote: ${files['hello.txt'].contents}`
|
||||
)
|
||||
})
|
||||
|
||||
it('returns contents of new files', function () {
|
||||
expect(snapshot.getDocContents('goodbye.txt')).to.equal(
|
||||
files['goodbye.txt'].contents
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('concurrency', function () {
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
specify('two concurrent inits', async function () {
|
||||
mockFlush({ repeat: 2 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
await Promise.all([snapshot.refresh(), snapshot.refresh()])
|
||||
|
||||
// The first request initializes, the second request loads changes
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
})
|
||||
|
||||
specify('three concurrent inits', async function () {
|
||||
mockFlush({ repeat: 2 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
await Promise.all([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// The first request initializes, the second and third are combined and
|
||||
// load changes
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
})
|
||||
|
||||
specify('two concurrent inits - first fails', async function () {
|
||||
mockFlush({ repeat: 2, failOnCall: call => call === 1 })
|
||||
mockLatestChunk()
|
||||
mockBlobs()
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// The first init fails, but the second succeeds
|
||||
expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(2)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(0)
|
||||
})
|
||||
|
||||
specify('three concurrent inits - second fails', async function () {
|
||||
mockFlush({ repeat: 4, failOnCall: call => call === 2 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// Another request afterwards
|
||||
await snapshot.refresh()
|
||||
|
||||
// The first init succeeds, the two queued requests fail, the last request
|
||||
// succeeds
|
||||
expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-2')).to.have.length(0)
|
||||
})
|
||||
|
||||
specify('two concurrent load changes', async function () {
|
||||
mockFlush({ repeat: 3 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
// Initialize
|
||||
await snapshot.refresh()
|
||||
|
||||
// Two concurrent load changes
|
||||
await Promise.all([snapshot.refresh(), snapshot.refresh()])
|
||||
|
||||
// One init, two load changes
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
|
||||
})
|
||||
|
||||
specify('three concurrent load changes', async function () {
|
||||
mockFlush({ repeat: 3 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
// Initialize
|
||||
await snapshot.refresh()
|
||||
|
||||
// Three concurrent load changes
|
||||
await Promise.all([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// One init, two load changes (the two last are queued and combined)
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
|
||||
})
|
||||
|
||||
specify('two concurrent load changes - first fails', async function () {
|
||||
mockFlush({ repeat: 3, failOnCall: call => call === 2 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
// Initialize
|
||||
await snapshot.refresh()
|
||||
|
||||
// Two concurrent load changes
|
||||
const results = await Promise.allSettled([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// One init, one load changes fails, the second succeeds
|
||||
expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(3)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-2')).to.have.length(0)
|
||||
})
|
||||
|
||||
specify('three concurrent load changes - second fails', async function () {
|
||||
mockFlush({ repeat: 4, failOnCall: call => call === 3 })
|
||||
mockLatestChunk()
|
||||
mockChanges()
|
||||
mockBlobs()
|
||||
|
||||
// Initialize
|
||||
await snapshot.refresh()
|
||||
|
||||
// Two concurrent load changes
|
||||
const results = await Promise.allSettled([
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
snapshot.refresh(),
|
||||
])
|
||||
|
||||
// Another request afterwards
|
||||
await snapshot.refresh()
|
||||
|
||||
// One init, one load changes succeeds, the second and third are combined
|
||||
// and fail, the last request succeeds
|
||||
expect(results.filter(r => r.status === 'fulfilled')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('flush')).to.have.length(4)
|
||||
expect(fetchMock.callHistory.calls('latest-chunk')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-1')).to.have.length(1)
|
||||
expect(fetchMock.callHistory.calls('changes-2')).to.have.length(1)
|
||||
})
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user