test: refactor
This commit is contained in:
parent
c820e67be6
commit
a627dbf933
|
@ -21,6 +21,7 @@ import {
|
|||
import { customEmojiIndex } from './customEmojiIndex'
|
||||
import { cleanEmoji } from './utils/cleanEmoji'
|
||||
import { loadDataForFirstTime, checkForUpdates } from './dataLoading'
|
||||
import { isSignalAborted } from './utils/isSignalAborted.js'
|
||||
|
||||
export default class Database {
|
||||
constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
|
||||
|
@ -36,17 +37,17 @@ export default class Database {
|
|||
}
|
||||
|
||||
async _init () {
|
||||
const controller = this._controller = new AbortController() // used to cancel ongoing requests if necessary
|
||||
const controller = this._controller = new AbortController() // used to cancel inflight requests if necessary
|
||||
const { signal } = controller
|
||||
const db = this._db = await openDatabase(this._dbName)
|
||||
addOnCloseListener(this._dbName, this._clear)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
const dataSource = this.dataSource
|
||||
const empty = await isEmpty(db)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { getETag, getETagAndData } from './utils/ajax'
|
||||
import { jsonChecksum } from './utils/jsonChecksum'
|
||||
import { hasData, loadData } from './idbInterface'
|
||||
import { isSignalAborted } from './utils/isSignalAborted.js'
|
||||
|
||||
export async function checkForUpdates (db, dataSource, signal) {
|
||||
// just do a simple HEAD request first to see if the eTags match
|
||||
let emojiData
|
||||
let eTag = await getETag(dataSource)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers
|
||||
const eTagAndData = await getETagAndData(dataSource)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -20,13 +21,13 @@ export async function checkForUpdates (db, dataSource, signal) {
|
|||
emojiData = eTagAndData[1]
|
||||
if (!eTag) {
|
||||
eTag = await jsonChecksum(emojiData)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
const doesHaveData = await hasData(db, dataSource, eTag)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -36,7 +37,7 @@ export async function checkForUpdates (db, dataSource, signal) {
|
|||
console.log('Database update available')
|
||||
if (!emojiData) {
|
||||
const eTagAndData = await getETagAndData(dataSource)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,7 @@ export async function checkForUpdates (db, dataSource, signal) {
|
|||
|
||||
export async function loadDataForFirstTime (db, dataSource, signal) {
|
||||
let [eTag, emojiData] = await getETagAndData(dataSource)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -56,7 +57,7 @@ export async function loadDataForFirstTime (db, dataSource, signal) {
|
|||
// Handle lack of support for ETag or Access-Control-Expose-Headers
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility
|
||||
eTag = await jsonChecksum(emojiData)
|
||||
if (signal.aborted) {
|
||||
if (isSignalAborted(signal)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
export let signalAbortedEventTarget
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
signalAbortedEventTarget = new EventTarget()
|
||||
}
|
||||
|
||||
export function isSignalAborted (signal) {
|
||||
/* istanbul ignore else */
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
// we need some way to test that someone called signal.aborted in our tests
|
||||
signalAbortedEventTarget.dispatchEvent(new CustomEvent('called'))
|
||||
}
|
||||
|
||||
return signal.aborted
|
||||
}
|
|
@ -7,74 +7,86 @@ import {
|
|||
tick, truncatedFrEmoji
|
||||
} from '../shared.js'
|
||||
import Database from '../../../src/database/Database.js'
|
||||
import { signalAbortedEventTarget } from '../../../src/database/utils/isSignalAborted.js'
|
||||
import { mockFetch, mockGetAndHead } from '../mockFetch.js'
|
||||
|
||||
const waitForSignalAbortCalledNTimes = async (n) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await Promise.race([
|
||||
new Promise(resolve => {
|
||||
signalAbortedEventTarget.addEventListener('called', resolve, { once: true })
|
||||
}),
|
||||
new Promise((resolve, reject) => setTimeout(() => reject(new Error('timed out waiting for signal.aborted call')), 1000))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function runTest ({ secondLoad, dataChanged, dataSource, signalAbortedCallCount }) {
|
||||
test(`throws no errors when DB is closed after ${signalAbortedCallCount} signal.aborted calls`, async () => {
|
||||
if (secondLoad) {
|
||||
// do a throwaway first load
|
||||
const db = new Database({ dataSource })
|
||||
await db.ready()
|
||||
await db.close()
|
||||
await tick(40)
|
||||
}
|
||||
|
||||
if (dataChanged) {
|
||||
// second time - update, data is v2
|
||||
fetch.reset()
|
||||
if (dataSource === ALL_EMOJI_NO_ETAG) {
|
||||
mockGetAndHead(dataSource, truncatedFrEmoji)
|
||||
} else if (dataSource === ALL_EMOJI_MISCONFIGURED_ETAG) {
|
||||
mockFetch('get', dataSource, truncatedFrEmoji, { headers: { ETag: 'W/yyy' } })
|
||||
mockFetch('head', dataSource, null)
|
||||
} else {
|
||||
mockGetAndHead(dataSource, truncatedFrEmoji, { headers: { ETag: 'W/yyy' } })
|
||||
}
|
||||
}
|
||||
|
||||
const db2 = new Database({ dataSource })
|
||||
await waitForSignalAbortCalledNTimes(signalAbortedCallCount)
|
||||
await db2.close()
|
||||
await tick(40)
|
||||
})
|
||||
}
|
||||
|
||||
describe('database timing tests', () => {
|
||||
beforeEach(basicBeforeEach)
|
||||
afterEach(basicAfterEach)
|
||||
|
||||
const dataChanges = [false, true]
|
||||
dataChanges.forEach(dataChanged => {
|
||||
describe(`dataChanged=${dataChanged}`, () => {
|
||||
const scenarios = [
|
||||
{
|
||||
testName: 'basic',
|
||||
dataSource: ALL_EMOJI
|
||||
},
|
||||
{
|
||||
testName: 'misconfigured etag server',
|
||||
dataSource: ALL_EMOJI_MISCONFIGURED_ETAG
|
||||
},
|
||||
{
|
||||
testName: 'missing etag server',
|
||||
dataSource: ALL_EMOJI_NO_ETAG
|
||||
}
|
||||
]
|
||||
scenarios.forEach(({ testName, dataSource }) => {
|
||||
describe(testName, () => {
|
||||
const secondLoads = [false, true]
|
||||
secondLoads.forEach(secondLoad => {
|
||||
describe(`secondLoad=${secondLoad}`, () => {
|
||||
const dataChangeds = secondLoad ? [false, true] : [false]
|
||||
dataChangeds.forEach(dataChanged => {
|
||||
describe(`dataChanged=${dataChanged}`, () => {
|
||||
const scenarios = [
|
||||
{
|
||||
testName: 'basic',
|
||||
dataSource: ALL_EMOJI,
|
||||
maxExpectedSignalAbortedCallCount: secondLoad ? (dataChanged ? 6 : 5) : 4
|
||||
},
|
||||
{
|
||||
testName: 'misconfigured etag',
|
||||
dataSource: ALL_EMOJI_MISCONFIGURED_ETAG,
|
||||
maxExpectedSignalAbortedCallCount: secondLoad ? 6 : 4
|
||||
},
|
||||
{
|
||||
testName: 'no etag',
|
||||
dataSource: ALL_EMOJI_NO_ETAG,
|
||||
maxExpectedSignalAbortedCallCount: secondLoad ? 7 : 5
|
||||
}
|
||||
]
|
||||
scenarios.forEach(({ testName, dataSource, maxExpectedSignalAbortedCallCount }) => {
|
||||
// Number of times somebody called the getter on `signal.aborted` which
|
||||
// we are using as an easy way to get full code coverage here
|
||||
const signalAbortedCallCounts = new Array(maxExpectedSignalAbortedCallCount).fill().map((_, i) => i)
|
||||
|
||||
let descriptor
|
||||
let onSignalAbortedCalled = new EventTarget()
|
||||
beforeEach(() => {
|
||||
descriptor = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')
|
||||
Object.defineProperty(AbortSignal.prototype, 'aborted', {
|
||||
get() {
|
||||
console.info('signal dot aborted called')
|
||||
onSignalAbortedCalled.dispatchEvent(new CustomEvent('signal-dot-aborted'))
|
||||
return descriptor.get.apply(this)
|
||||
},
|
||||
set(val) {
|
||||
return descriptor.set.apply(this, val)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(AbortSignal.prototype, 'aborted', descriptor)
|
||||
})
|
||||
|
||||
// Number of times somebody called the getter on `signal.aborted` which
|
||||
// we are using as an easy way to get full code coverage here
|
||||
const signalAbortedCheckCounts = new Array(5).fill().map((_, i) => i)
|
||||
signalAbortedCheckCounts.forEach(count => {
|
||||
test(`throws no errors when DB is canceled after ${count} ticks`, async () => {
|
||||
// first load
|
||||
const db = new Database({ dataSource })
|
||||
await tick(count)
|
||||
await db.close()
|
||||
await tick(40)
|
||||
|
||||
if (dataChanged) {
|
||||
// second time - update, data is v2
|
||||
fetch.reset()
|
||||
fetch.get(dataSource, () => new Response(JSON.stringify(truncatedFrEmoji), { headers: { ETag: 'W/yyy' } }), { delay: 1 })
|
||||
fetch.head(dataSource, () => new Response(null, { headers: { ETag: 'W/yyy' } }), { delay: 1 })
|
||||
}
|
||||
|
||||
// second load
|
||||
const db2 = new Database({ dataSource })
|
||||
await tick(count)
|
||||
await db2.close()
|
||||
await tick(40)
|
||||
signalAbortedCallCounts.forEach(signalAbortedCallCount => {
|
||||
describe(testName, () => {
|
||||
runTest({ secondLoad, dataChanged, dataSource, signalAbortedCallCount })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue