From f5f501c34e0fcaab2a4e83e6a1516bce5dd405eb Mon Sep 17 00:00:00 2001 From: Dara Hayes Date: Thu, 31 Oct 2019 15:21:17 +0000 Subject: [PATCH] fix: refactor offix-client integration test files (#223) * Re-factored tests * TypeScript linting * fix: lint issues in tests * chore: temporarily disable offix-client integration tests * ci: remove explicit integration test command Co-authored-by: Joseph Quevedo --- .circleci/config.yml | 8 +- .../integration_test/karma.conf.js | 74 --- .../integration_test/server/graphql.js | 34 -- .../integration_test/server/index.js | 29 - .../integration_test/test/offline.test.js | 449 --------------- .../integration_test/testfile.png | 1 - .../integration_test/testfile.txt | 1 - .../integration_test/utils/network.js | 32 -- .../integration_test/utils/server.js | 21 - packages/offix-client/jest.config.js | 3 +- packages/offix-client/package.json | 10 +- .../fixtures/test_file_contents.ts} | 4 +- .../integration/Cache.test.ts} | 192 +++---- .../integration/Conflict.test.ts} | 276 +++++---- .../test/integration/Offline.test.ts | 533 ++++++++++++++++++ packages/offix-client/test/server/graphql.ts | 31 + packages/offix-client/test/server/index.ts | 29 + .../schema.js => test/server/schema.ts} | 34 +- .../utils/graphql.queries.ts} | 4 +- .../utils/mocha.js => test/utils/mocha.ts} | 2 +- packages/offix-client/test/utils/network.ts | 35 ++ packages/offix-client/test/utils/server.ts | 21 + .../utils/testApolloLink.ts} | 4 +- .../testStore.js => test/utils/testStore.ts} | 20 +- .../timeout.js => test/utils/timeout.ts} | 2 +- .../waitFor.js => test/utils/waitFor.ts} | 2 +- 26 files changed, 924 insertions(+), 927 deletions(-) delete mode 100644 packages/offix-client/integration_test/karma.conf.js delete mode 100644 packages/offix-client/integration_test/server/graphql.js delete mode 100644 packages/offix-client/integration_test/server/index.js delete mode 100644 packages/offix-client/integration_test/test/offline.test.js delete mode 100644 packages/offix-client/integration_test/testfile.png delete mode 100644 packages/offix-client/integration_test/testfile.txt delete mode 100644 packages/offix-client/integration_test/utils/network.js delete mode 100644 packages/offix-client/integration_test/utils/server.js rename packages/offix-client/{integration_test/test/fixtures/test_file_contents.js => test/fixtures/test_file_contents.ts} (98%) rename packages/offix-client/{integration_test/test/cache.test.js => test/integration/Cache.test.ts} (68%) rename packages/offix-client/{integration_test/test/conflict.test.js => test/integration/Conflict.test.ts} (57%) create mode 100644 packages/offix-client/test/integration/Offline.test.ts create mode 100644 packages/offix-client/test/server/graphql.ts create mode 100644 packages/offix-client/test/server/index.ts rename packages/offix-client/{integration_test/server/schema.js => test/server/schema.ts} (80%) rename packages/offix-client/{integration_test/utils/graphql.queries.js => test/utils/graphql.queries.ts} (98%) rename packages/offix-client/{integration_test/utils/mocha.js => test/utils/mocha.ts} (98%) create mode 100644 packages/offix-client/test/utils/network.ts create mode 100644 packages/offix-client/test/utils/server.ts rename packages/offix-client/{integration_test/utils/testApolloLink.js => test/utils/testApolloLink.ts} (68%) rename packages/offix-client/{integration_test/utils/testStore.js => test/utils/testStore.ts} (59%) rename packages/offix-client/{integration_test/utils/timeout.js => test/utils/timeout.ts} (77%) rename packages/offix-client/{integration_test/utils/waitFor.js => test/utils/waitFor.ts} (86%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 66a5b7286..6be07adb3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,9 +35,11 @@ jobs: - run: name: test command: npm test - - run: - name: integration test - command: cd packages/offix-client && npm run integration-test + # No longer have integration tests as a separate command + # We may reintroduce it in the future + # - run: + # name: integration test + # command: cd packages/offix-client && npm run integration-test - store_artifacts: path: test-results.xml prefix: tests diff --git a/packages/offix-client/integration_test/karma.conf.js b/packages/offix-client/integration_test/karma.conf.js deleted file mode 100644 index b952a5e9a..000000000 --- a/packages/offix-client/integration_test/karma.conf.js +++ /dev/null @@ -1,74 +0,0 @@ -// Karma configuration -// Generated on Fri Jan 04 2019 09:28:53 GMT+0100 (GMT+01:00) - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'chai'], - - - // list of files / patterns to load in the browser - files: [ - 'test/*.test.js' - ], - - - // list of files / patterns to exclude - exclude: [ - ], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'test/*.test.js': ['webpack'] - }, - - - webpack: { - mode: 'development' - }, - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['mocha'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['ChromeHeadless'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: true, - - // Concurrency level - // how many browser should be started simultaneous - concurrency: Infinity - }) -} \ No newline at end of file diff --git a/packages/offix-client/integration_test/server/graphql.js b/packages/offix-client/integration_test/server/graphql.js deleted file mode 100644 index 878ec8710..000000000 --- a/packages/offix-client/integration_test/server/graphql.js +++ /dev/null @@ -1,34 +0,0 @@ -const express = require('express'); -const { VoyagerServer } = require('@aerogear/voyager-server'); -const http = require('http'); -const { SubscriptionServer } = require('subscriptions-transport-ws') -const { execute, subscribe } = require('graphql') - -const { typeDefs, resolvers } = require('./schema'); - -const PORT = 4000; - -function start() { - const app = express(); - - const apolloServer = VoyagerServer({ typeDefs, resolvers }); - const httpServer = http.createServer(app); - apolloServer.applyMiddleware({ app }); - - return new Promise(resolve => { - httpServer.listen({ port: PORT }, async () => { - new SubscriptionServer({ - execute, - subscribe, - schema: apolloServer.schema - }, { - server: httpServer, - path: '/graphql' - }); - console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`); - resolve(httpServer); - }); - }); -} - -module.exports = start; \ No newline at end of file diff --git a/packages/offix-client/integration_test/server/index.js b/packages/offix-client/integration_test/server/index.js deleted file mode 100644 index 3f95928e6..000000000 --- a/packages/offix-client/integration_test/server/index.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express'); -const cors = require('cors'); - -const graphqlStart = require('./graphql'); -const resetData = require('./schema').resetData; - -const app = express(); -app.use(cors()); - -const PORT = 4001; - -let graphqlServer; - -app.post('/start', async (_, res) => { - graphqlServer = await graphqlStart(); - res.sendStatus(200); -}); -app.post('/stop', (_, res) => { - if (graphqlServer) { - graphqlServer.close(); - } - res.sendStatus(200); -}); -app.post('/reset', (_, res) => { - resetData(); - res.sendStatus(200); -}); - -app.listen(PORT, () => console.log(`Server listening on port ${PORT}!`)); diff --git a/packages/offix-client/integration_test/test/offline.test.js b/packages/offix-client/integration_test/test/offline.test.js deleted file mode 100644 index 92bb652d7..000000000 --- a/packages/offix-client/integration_test/test/offline.test.js +++ /dev/null @@ -1,449 +0,0 @@ -import { createClient } from '../../dist' -import { createOptimisticResponse, CacheOperation } from 'offix-cache'; -import { TestStore } from '../utils/testStore'; -import { ToggleableNetworkStatus } from '../utils/network'; -import server from '../utils/server'; -import { ADD_TASK, GET_TASKS, UPDATE_TASK, DELETE_TASK } from '../utils/graphql.queries'; - -function wait(time) { - return new Promise((resolve, reject) => { - setTimeout(resolve, time) - }) -} - -// TODO: error handling when server is down - -const newNetworkStatus = (online = true) => { - const networkStatus = new ToggleableNetworkStatus(); - networkStatus.setOnline(online); - return networkStatus; -}; - -const offlineMetaKey = "offline-meta-data"; - -const newClient = async (clientOptions = {}) => { - const config = { - httpUrl: "http://localhost:4000/graphql", - wsUrl: "ws://localhost:4000/graphql", - ...clientOptions - }; - - return await createClient(config); -}; - -describe('Offline mutations', function () { - - this.timeout(20000); - - const mutationsQueueName = 'offline-mutation-queue'; - - const newTask = { - description: 'new', - title: 'new', - version: 1, - author: "new" - }; - - const optimisticOptions = { - mutation: ADD_TASK, - operationType: CacheOperation.ADD, - returnType: "Task", - variables: newTask, - updateQuery: GET_TASKS - }; - - const updatedTask = { - description: 'updated', - title: 'updated' - }; - - let client, networkStatus, store; - - before('start server', async function () { - await server.start(); - }); - - after('stop server', async function () { - await server.stop(); - }); - - beforeEach('reset server', async function () { - await server.reset(); - }); - - beforeEach('create client', async function () { - networkStatus = newNetworkStatus(false); - store = new TestStore(); - client = await newClient({ networkStatus, storage: store, mutationsQueueName }); - }); - - async function isQueueEmpty() { - const store = await store.getItem(offlineMetaKey); - return store.length === 0 - } - - describe('offline mutations', function () { - - it('mutations are queued when offline', async function () { - const mutationOptions = { - mutation: ADD_TASK, - variables: newTask - } - await client.offlineMutate(mutationOptions).catch(err => { }); - - const queue = client.queue.queue - expect(queue.length).to.equal(1) - const op = queue[0].operation.op - expect(op.mutation).to.deep.equal(mutationOptions.mutation) - expect(op.variables).to.deep.equal(mutationOptions.variables) - }); - - it('mutations are persisted while offline', async function () { - const mutationOptions = { - mutation: ADD_TASK, - variables: newTask - } - - await client.offlineMutate(mutationOptions).catch(err => {}) - - const store = client.queue.store - const storeData = await store.getOfflineData() - - expect(storeData.length).to.equal(1); - - const op = storeData[0].operation.op - expect(op.mutation).to.deep.equal(mutationOptions.mutation) - expect(op.variables).to.deep.equal(mutationOptions.variables) - }); - - it('offlineMutate throws an offline error, we can get result when coming back online', function (done) { - client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask - }).catch((err) => { - const promise = err.offlineMutatePromise - networkStatus.setOnline(true); // go online - promise.then((response) => { - console.log('offline changes replicated', response) - expect(response.data.createTask).to.exist; - expect(response.data.createTask.title).to.equal(newTask.title); - expect(response.data.createTask.description).to.equal(newTask.description); - done() - }) - }) - }); - - it('queues multiple mutations while offline', async function () { - networkStatus.setOnline(true); - - const response = await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask - }); - - let task = response.data.createTask; - - networkStatus.setOnline(false); - - const variables = { ...updatedTask, id: task.id, version: task.version }; - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - variables - }); - } catch (ignore) { } - - try { - await client.offlineMutate({ - mutation: DELETE_TASK, - variables: { id: task.id } - }); - } catch (ignore) { } - - const queue = client.queue.queue - expect(queue.length).to.equal(2) - expect(queue[0].operation.op.context.operationName).to.equal('updateTask'); - expect(queue[1].operation.op.context.operationName).to.equal('deleteTask'); - }); - - it('Handles multiple mutations on the same object in queue/store', async function () { - networkStatus.setOnline(true); - - const result = await client.mutate({ - mutation: ADD_TASK, - variables: newTask - }) - - networkStatus.setOnline(false); - - const task = result.data.createTask; - const variables = { ...task, ...updatedTask }; - - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - variables - }); - } catch (ignore) { } - try { - await client.offlineMutate({ - mutation: DELETE_TASK, - variables: { id: task.id } - }); - } catch (ignore) { } - - networkStatus.setOnline(true); - - await new Promise(resolve => setTimeout(resolve, 300)); - - const response = await client.query({ - query: GET_TASKS, - fetchPolicy: 'network-only' - }); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(0); - }); - - it('can create new items and subsequently mutate them while offline, async function ()', async () => { - let task; - const optimisticResponse = createOptimisticResponse(optimisticOptions) - try { - await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask, - optimisticResponse, - update: (_, { data: { createTask } }) => task = createTask - }) - } catch (ignore) { } - - const variables = { ...updatedTask, id: task.id, version: task.version }; - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - variables - }); - - } catch (ignore) { } - networkStatus.setOnline(true); - - await new Promise(resolve => setTimeout(resolve, 300)); - - const response = await client.query({ - query: GET_TASKS, - fetchPolicy: 'network-only' - }) - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal(updatedTask.title); - }); - - it('reinitialized client will replay operations from the offline storage (simulates an app restart)', async function () { - let task; - try { - await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask, - optimisticResponse: createOptimisticResponse(optimisticOptions), - update: (_, { data: { createTask } }) => task = createTask - }); - } catch (ignore) { } - - const variables = { ...updatedTask, id: task.id, version: task.version }; - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - variables - }); - } catch (ignore) { } - - networkStatus = newNetworkStatus(); - client = await newClient({ networkStatus, storage: store, mutationsQueueName }); - - await new Promise(resolve => setTimeout(resolve, 300)); - - const response = await client.query({ - query: GET_TASKS, - fetchPolicy: 'network-only' - }); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal(updatedTask.title); - }); - - it('it can handle multiple offline mutations on an existing object and replay them successfully', async function () { - networkStatus.setOnline(true); - - const result = await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask, - returnType: "Task", - optimisticResponse: createOptimisticResponse(optimisticOptions), - }); - - let task = result.data.createTask; - - networkStatus.setOnline(false); - - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - returnType: 'Task', - variables: { title: 'update1', description: 'merge', id: task.id, version: task.version } - }); - } catch (err) { } - - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - returnType: 'Task', - variables: { title: 'update2', description: 'merge', id: task.id, version: task.version } - }); - } catch (err) { } - - networkStatus.setOnline(true); - - await wait(1000); - - const response = await client.query({ - query: GET_TASKS, - fetchPolicy: 'network-only' - }); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('update2'); - }); - - }) - - describe('do not merge offline mutations', function () { - - let task; - - beforeEach('prepare data', async function () { - client = await newClient({ networkStatus, storage: store, mutationsQueueName }); - networkStatus.setOnline(true); - - const response = await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask, - optimisticResponse: createOptimisticResponse(optimisticOptions), - }); - - task = response.data.createTask; - - networkStatus.setOnline(false); - }); - - it('should succeed', async function () { - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - returnType: "Task", - variables: { title: 'nomerge1', description: 'nomerge', id: task.id, version: task.version } - }); - } catch (ignore) { } - - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - returnType: "Task", - variables: { title: 'nomerge2', description: 'nomerge', id: task.id, version: task.version } - }); - } catch (ignore) { } - - const offlineData = await client.queue.store.getOfflineData() - - expect(offlineData.length).to.equal(2) - - - expect(offlineData[0]).to.exist; - expect(offlineData[1]).to.exist; - expect(offlineData[0].operation.op.variables.title).to.equal('nomerge1'); - expect(offlineData[1].operation.op.variables.title).to.equal('nomerge2'); - - networkStatus.setOnline(true); - - await wait(300); - - const response = await client.query({ - query: GET_TASKS, - fetchPolicy: 'network-only' - }); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('nomerge2'); - }); - }); - - describe('ensure offline params are added to offline object ', function () { - - let task; - - beforeEach('prepare data', async function () { - client = await newClient({ networkStatus, storage: store, mutationsQueueName }); - networkStatus.setOnline(true); - - const response = await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask, - optimisticResponse: createOptimisticResponse(optimisticOptions), - }); - - task = response.data.createTask; - - networkStatus.setOnline(false); - }); - - it('should succeed', async function () { - const variables = { title: 'nomerge1', description: 'nomerge', id: task.id, version: task.version }; - try { - await client.offlineMutate({ - mutation: UPDATE_TASK, - returnType: "Task", - variables, - idField: "someOtherField", - returnType: "ADifferentType" - }); - } catch (ignore) { } - - const offlineData = await client.queue.store.getOfflineData() - const op = offlineData[0].operation.op - expect(op.context.returnType).to.equal('ADifferentType'); - expect(op.context.idField).to.equal('someOtherField'); - }); - }); - - describe('notify about offline changes', function () { - it('should succeed', async function () { - let offlineOps = 0; - let cleared = 0; - - const listener = { - onOperationEnqueued: () => offlineOps++, - queueCleared: () => cleared++ - }; - - client = await newClient({ networkStatus, storage: store, mutationsQueueName, offlineQueueListener: listener }); - try { - await client.offlineMutate({ - mutation: ADD_TASK, - variables: newTask - }); - } catch (ignore) { } - - expect(offlineOps).to.equal(1); - - networkStatus.setOnline(true); - - await new Promise(resolve => setTimeout(resolve, 300)); - - expect(cleared).to.equal(1); - }); - }); -}); - - diff --git a/packages/offix-client/integration_test/testfile.png b/packages/offix-client/integration_test/testfile.png deleted file mode 100644 index f7cf3e06f..000000000 --- a/packages/offix-client/integration_test/testfile.png +++ /dev/null @@ -1 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras quis blandit lectus, non vulputate augue. Nunc lobortis ut augue a vestibulum. In eget ultricies magna. Integer maximus pulvinar eros, gravida ornare magna faucibus sit amet. Phasellus dolor turpis, pellentesque vitae sagittis id, posuere et odio. Nam ac magna non lorem lacinia dictum. Nullam molestie ligula id venenatis posuere. Morbi semper, erat ac laoreet convallis, augue erat ultricies purus, in auctor enim tellus sed lacus. Phasellus id enim leo. Integer accumsan, dui ut ornare ullamcorper, nisl mauris sodales diam, vulputate venenatis mi elit sit amet velit. Curabitur tellus eros, auctor sit amet aliquam eget, porta id erat. Fusce pellentesque ut nisl quis scelerisque. \ No newline at end of file diff --git a/packages/offix-client/integration_test/testfile.txt b/packages/offix-client/integration_test/testfile.txt deleted file mode 100644 index a256b6b23..000000000 --- a/packages/offix-client/integration_test/testfile.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/packages/offix-client/integration_test/utils/network.js b/packages/offix-client/integration_test/utils/network.js deleted file mode 100644 index 142b61539..000000000 --- a/packages/offix-client/integration_test/utils/network.js +++ /dev/null @@ -1,32 +0,0 @@ -const onLineGetter = window.navigator.__lookupGetter__('onLine'); - -export const goOffLine = () => { - window.navigator.__defineGetter__('onLine', () => false); -}; - -export const goOnLine = () => { - window.navigator.__defineGetter__('onLine', onLineGetter); -}; - -export class ToggleableNetworkStatus { - constructor() { - this.online = true; - this.callbacks = [] - } - - onStatusChangeListener(callback) { - this.callbacks.push(callback) - } - - isOffline() { - const online = this.online; - return new Promise(resolve => resolve(!online)); - } - - setOnline(online) { - this.online = online; - for (const callback of this.callbacks) { - callback.onStatusChange({ online }) - } - } -}; diff --git a/packages/offix-client/integration_test/utils/server.js b/packages/offix-client/integration_test/utils/server.js deleted file mode 100644 index 68db759fe..000000000 --- a/packages/offix-client/integration_test/utils/server.js +++ /dev/null @@ -1,21 +0,0 @@ -function manage(cmd) { - return fetch(`http://localhost:4001/${cmd}`, { method: 'POST' }); -} - -function start() { - return manage('start'); -} - -function stop() { - return manage('stop'); -} - -function reset() { - return manage('reset'); -} - -module.exports = { - start, - stop, - reset -}; diff --git a/packages/offix-client/jest.config.js b/packages/offix-client/jest.config.js index c453abf34..ad06f44e1 100644 --- a/packages/offix-client/jest.config.js +++ b/packages/offix-client/jest.config.js @@ -142,7 +142,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - "**/test/*.test.ts" + "**/test/*.test.ts", + // "**/test/integration/*.test.ts" // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" ], diff --git a/packages/offix-client/package.json b/packages/offix-client/package.json index 23150cb32..7ac1f025c 100644 --- a/packages/offix-client/package.json +++ b/packages/offix-client/package.json @@ -8,9 +8,7 @@ "clean": "del coverage src/**/*.js src/**/*.map test/**/*.js test/**/*.map dist types", "build": "tsc", "watch": "tsc --watch", - "test": "jest", - "preintegration-test": "cd integration_test && yarn install", - "integration-test": "cd integration_test && yarn run test" + "test": "jest" }, "license": "Apache-2.0", "repository": { @@ -22,21 +20,17 @@ }, "devDependencies": { "@types/apollo-upload-client": "8.1.3", - "@types/chai": "4.2.4", "@types/debug": "4.1.5", "@types/fetch-mock": "7.3.1", "@types/graphql": "14.2.3", - "@types/jest": "24.0.21", - "@types/mocha": "5.2.7", + "@types/jest": "24.0.18", "@types/ws": "6.0.3", - "chai": "4.2.0", "del": "5.1.0", "fake-indexeddb": "2.1.1", "fetch-mock": "7.7.0", "graphql": "14.5.8", "graphql-tag": "2.10.1", "jest": "24.9.0", - "mocha": "6.2.2", "ts-jest": "24.1.0", "ts-node": "8.4.1", "typescript": "3.6.4" diff --git a/packages/offix-client/integration_test/test/fixtures/test_file_contents.js b/packages/offix-client/test/fixtures/test_file_contents.ts similarity index 98% rename from packages/offix-client/integration_test/test/fixtures/test_file_contents.js rename to packages/offix-client/test/fixtures/test_file_contents.ts index cc71f1fd9..4313b8d1e 100644 --- a/packages/offix-client/integration_test/test/fixtures/test_file_contents.js +++ b/packages/offix-client/test/fixtures/test_file_contents.ts @@ -1,2 +1,2 @@ -export const textFileContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras quis blandit lectus, non vulputate augue. Nunc lobortis ut augue a vestibulum. In eget ultricies magna. Integer maximus pulvinar eros, gravida ornare magna faucibus sit amet. Phasellus dolor turpis, pellentesque vitae sagittis id, posuere et odio. Nam ac magna non lorem lacinia dictum. Nullam molestie ligula id venenatis posuere. Morbi semper, erat ac laoreet convallis, augue erat ultricies purus, in auctor enim tellus sed lacus. Phasellus id enim leo. Integer accumsan, dui ut ornare ullamcorper, nisl mauris sodales diam, vulputate venenatis mi elit sit amet velit. Curabitur tellus eros, auctor sit amet aliquam eget, porta id erat. Fusce pellentesque ut nisl quis scelerisque." -export const encodedImage = "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" \ No newline at end of file +export const textFileContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras quis blandit lectus, non vulputate augue. Nunc lobortis ut augue a vestibulum. In eget ultricies magna. Integer maximus pulvinar eros, gravida ornare magna faucibus sit amet. Phasellus dolor turpis, pellentesque vitae sagittis id, posuere et odio. Nam ac magna non lorem lacinia dictum. Nullam molestie ligula id venenatis posuere. Morbi semper, erat ac laoreet convallis, augue erat ultricies purus, in auctor enim tellus sed lacus. Phasellus id enim leo. Integer accumsan, dui ut ornare ullamcorper, nisl mauris sodales diam, vulputate venenatis mi elit sit amet velit. Curabitur tellus eros, auctor sit amet aliquam eget, porta id erat. Fusce pellentesque ut nisl quis scelerisque."; +export const encodedImage = "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"; diff --git a/packages/offix-client/integration_test/test/cache.test.js b/packages/offix-client/test/integration/Cache.test.ts similarity index 68% rename from packages/offix-client/integration_test/test/cache.test.js rename to packages/offix-client/test/integration/Cache.test.ts index abefdb9e1..76bac9f81 100644 --- a/packages/offix-client/integration_test/test/cache.test.js +++ b/packages/offix-client/test/integration/Cache.test.ts @@ -1,14 +1,14 @@ -import server from '../utils/server'; -import { createClient } from '../../dist'; -import { CacheOperation, getUpdateFunction, createOptimisticResponse } from '../../../offix-cache/dist'; -import { ToggleableNetworkStatus } from '../utils/network'; -import { ADD_TASK, GET_TASKS, DELETE_TASK, FIND_TASK_BY_TITLE, GET_TASK } from '../utils/graphql.queries'; -import { TestStore } from '../utils/testStore'; +import { CacheOperation, createOptimisticResponse, getUpdateFunction } from "offix-cache"; +import { createClient } from "../../types"; +import { ADD_TASK, DELETE_TASK, FIND_TASK_BY_TITLE, GET_TASK, GET_TASKS } from "../utils/graphql.queries"; +import { ToggleableNetworkStatus } from "../utils/network"; +import server from "../utils/server"; +import { TestStore } from "../utils/testStore"; -const CLIENT_HTTP_URL = 'http://localhost:4000/graphql'; -const CLIENT_WS_URL = 'ws://localhost:4000/graphql'; +const CLIENT_HTTP_URL = "http://localhost:4000/graphql"; +const CLIENT_WS_URL = "ws://localhost:4000/graphql"; -const newClient = async (options) => { +const newClient = async (options?) => { const networkStatus = new ToggleableNetworkStatus(); const storage = new TestStore(); const client = await createClient({ @@ -19,8 +19,8 @@ const newClient = async (options) => { ...options }); - return { client, networkStatus, storage } -} + return { client, networkStatus, storage }; +}; const goOffline = (networkStatus) => { networkStatus.setOnline(false); @@ -30,38 +30,38 @@ const goOnline = (networkStatus) => { networkStatus.setOnline(true); }; -const CACHE_ONLY = 'cache-only'; -const NETWORK_ONLY = 'network-only'; +const CACHE_ONLY = "cache-only"; +const NETWORK_ONLY = "network-only"; -const TASK_TYPE = 'Task'; +const TASK_TYPE = "Task"; const TASK_TEMPLATE = { - title: 'Unique', - description: 'BlaBlaBla', + title: "Unique", + description: "BlaBlaBla", author: "StephenCoady", - version: 1, + version: 1 }; const FIND_TASK_BY_TITLE_QUERY = { query: FIND_TASK_BY_TITLE, variables: { - title: TASK_TEMPLATE.title, - }, + title: TASK_TEMPLATE.title + } }; -const getTasks = async (client, options) => { +const getTasks = async (client, options?) => { return await client.query({ query: GET_TASKS, - ...options, + ...options }); -} +}; -const findTaskByTitle = async (client, options) => { +const findTaskByTitle = async (client, options?) => { return await client.query({ ...FIND_TASK_BY_TITLE_QUERY, ...options }); -} +}; const offlineMutateWhileOffline = async (client, options) => { try { @@ -74,60 +74,59 @@ const offlineMutateWhileOffline = async (client, options) => { throw e; } } - throw 'offlineMutate didn\'t throw OfflineError'; -} + throw new Error("offlineMutate didn't throw OfflineError"); +}; -const addTaskWhileOffline = async (client, options) => { +const addTaskWhileOffline = async (client, options?) => { return await offlineMutateWhileOffline(client, { mutation: ADD_TASK, variables: TASK_TEMPLATE, returnType: TASK_TYPE, - ...options, + ...options }); }; -const assertTaskEqualTaskTemplate = (task, template) => { +const assertTaskEqualTaskTemplate = (task, template?) => { template = { ...TASK_TEMPLATE, ...template }; - expect(task).to.exist; - + expect(task).toBeDefined(); if (template.id == null) { - expect(task.id).to.exist; + expect(task.id).toBeDefined(); } else { - expect(task.id).to.equal(template.id); + expect(task.id).toBe(template.id); } - expect(task.title).to.equal(template.title); - expect(task.description).to.equal(template.description); - expect(task.version).to.equal(template.version); + expect(task.title).toBe(template.title); + expect(task.description).toBe(template.description); + expect(task.version).toBe(template.version); }; describe("Offline cache and mutations", () => { - before('start server', async function () { + before("start server", async function() { await server.start(); }); - after('stop server', async function () { + after("stop server", async function() { await server.stop(); }); - beforeEach('reset server', async function () { + beforeEach("reset server", async function() { await server.reset(); }); - describe('update list query while offline', () => { + describe("update list query while offline", () => { - it('query tasks while online, go offline, create task and query tasks again using mutationCacheUpdates and updateQuery', async function () { + it("query tasks while online, go offline, create task and query tasks again using mutationCacheUpdates and updateQuery", async function() { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: GET_TASKS - }), - }, - }) + }) + } + }); // search for tasks while online await getTasks(client); @@ -139,21 +138,21 @@ describe("Offline cache and mutations", () => { // search for tasks again while offline const response = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response.data.allTasks[0]); }); - it('query tasks while online, go offline, create task and query tasks again using mutationCacheUpdates', async function () { + it("query tasks while online, go offline, create task and query tasks again using mutationCacheUpdates", async function() { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: GET_TASKS }) - }, - }) + } + }); // search for tasks while online await getTasks(client); @@ -166,14 +165,14 @@ describe("Offline cache and mutations", () => { // search for tasks again while offline const response = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response.data.allTasks[0]); }); - it('query tasks while online, go offline, create task and query tasks again using updateQuery', async function () { + it("query tasks while online, go offline, create task and query tasks again using updateQuery", async function() { - const { client, networkStatus: network } = await newClient() + const { client, networkStatus: network } = await newClient(); // search for tasks while online await getTasks(client); @@ -186,20 +185,20 @@ describe("Offline cache and mutations", () => { // search for tasks again while offline const response = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response.data.allTasks[0]); }); - it('create task and query tasks while offline using mutationCacheUpdates', async function () { + it("create task and query tasks while offline using mutationCacheUpdates", async function() { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: GET_TASKS - }), + }) } - }) + }); goOffline(network); @@ -208,15 +207,15 @@ describe("Offline cache and mutations", () => { // search for tasks again while offline const response = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response.data.allTasks[0]); }); // Not currently supported - it.skip('create task and query tasks while offline using updateQuery', async function () { + it.skip("create task and query tasks while offline using updateQuery", async function() { - const { client, networkStatus: network } = await newClient({}) + const { client, networkStatus: network } = await newClient({}); goOffline(network); @@ -226,19 +225,19 @@ describe("Offline cache and mutations", () => { // search for tasks again while offline const response = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response.data.allTasks[0]); }); }); - describe('update single object query while offline', () => { + describe("update single object query while offline", () => { // Not supported - it.skip('create task and the search it by title while offline', async () => { + it.skip("create task and the search it by title while offline", async () => { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: FIND_TASK_BY_TITLE_QUERY }) } @@ -255,13 +254,13 @@ describe("Offline cache and mutations", () => { assertTaskEqualTaskTemplate(result1.data.findTaskByTitle); }); - it.skip('search task by title while online, than go offline, create task and search for it again', async () => { + it.skip("search task by title while online, than go offline, create task and search for it again", async () => { const { client, networkStatus: network } = await newClient(); // search the task while online const result1 = await findTaskByTitle(client); - expect(result1.data.findTaskByTitle).to.not.exist; + expect(result1.data.findTaskByTitle).toBeFalsy(); goOffline(network); @@ -274,14 +273,14 @@ describe("Offline cache and mutations", () => { assertTaskEqualTaskTemplate(result2.data.findTaskByTitle); }); - it.skip('create task and search for it while offline using mutationCacheUpdates', async () => { + it.skip("create task and search for it while offline using mutationCacheUpdates", async () => { // Impossible situation if applied to real world use case const { client, networkStatus } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: FIND_TASK_BY_TITLE_QUERY }) } @@ -300,17 +299,16 @@ describe("Offline cache and mutations", () => { assertTaskEqualTaskTemplate(result1); }); - - it.skip('create a task and query it by id while offline', async () => { + it.skip("create a task and query it by id while offline", async () => { const { client, networkStatus } = await newClient(); const optimistic = createOptimisticResponse({ mutation: ADD_TASK, variables: TASK_TEMPLATE, - returnType: 'Task', + returnType: "Task", operationType: CacheOperation.ADD, - idField: 'id', + idField: "id" }); goOffline(networkStatus); @@ -320,7 +318,7 @@ describe("Offline cache and mutations", () => { optimisticResponse: optimistic, updateQuery: { query: GET_TASK, - variables: { d: optimistic.id }, + variables: { d: optimistic.id } } }); @@ -328,29 +326,29 @@ describe("Offline cache and mutations", () => { const result = await client.query({ query: GET_TASK, variables: { id: optimistic.id }, - fetchPolicy: 'cache-only' + fetchPolicy: "cache-only" }); assertTaskEqualTaskTemplate(result.data.getTask, { id: optimistic.id }); }); }); - describe('update list query while offline and then go back online', () => { + describe("update list query while offline and then go back online", () => { it.skip("query tasks while online, go offline, create task, delete task, go back online using mutationCacheUpdates", async () => { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: GET_TASKS }), deleteTask: getUpdateFunction({ - mutationName: 'deleteTask', + mutationName: "deleteTask", updateQuery: GET_TASKS, operationType: CacheOperation.DELETE }) - }, + } }); // search tasks while online @@ -364,8 +362,8 @@ describe("Offline cache and mutations", () => { // retrieve the new task from the cache const response1 = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response1.data.allTasks).to.exist; - expect(response1.data.allTasks.length).to.equal(1); + expect(response1.data.allTasks).toBeDefined(); + expect(response1.data.allTasks.length).toBe(1); assertTaskEqualTaskTemplate(response1.data.allTasks[0]); const task = response1.data.allTasks[0]; @@ -375,12 +373,14 @@ describe("Offline cache and mutations", () => { variables: { id: task.id }, operationType: CacheOperation.DELETE, returnType: TASK_TYPE, - updateQuery: GET_TASKS, - }).catch (ignore => {}); + updateQuery: GET_TASKS + }).catch ((_) => { + // ignore + }); // query tasks while still offline const response2 = await getTasks(client, { fetchPolicy: CACHE_ONLY }); - expect(response2.data.allTasks).to.exist; - expect(response2.data.allTasks.length).to.equal(0); + expect(response2.data.allTasks).toBeDefined(); + expect(response2.data.allTasks.length).toBe(0); goOnline(network); @@ -389,8 +389,8 @@ describe("Offline cache and mutations", () => { // query tasks again from the cache const response3 = await getTasks(client, {fetchPolicy: CACHE_ONLY}); - expect(response3.data.allTasks).to.exist; - expect(response3.data.allTasks.length).to.equal(0); + expect(response3.data.allTasks).toBeDefined(); + expect(response3.data.allTasks.length).toBe(0); }); it("query tasks while online, go offline, create task, delete task, go back online using mutationCacheUpdates", async () => { @@ -398,10 +398,10 @@ describe("Offline cache and mutations", () => { const { client, networkStatus: network } = await newClient({ mutationCacheUpdates: { createTask: getUpdateFunction({ - mutationName: 'createTask', + mutationName: "createTask", updateQuery: GET_TASKS }) - }, + } }); // search tasks while online @@ -422,7 +422,7 @@ describe("Offline cache and mutations", () => { // wait for all offline transactions to be executed const replicatedData = await addTaskError.watchOfflineChange(); - assertTaskEqualTaskTemplate(replicatedData.data.createTask) + assertTaskEqualTaskTemplate(replicatedData.data.createTask); }); - }) + }); }); diff --git a/packages/offix-client/integration_test/test/conflict.test.js b/packages/offix-client/test/integration/Conflict.test.ts similarity index 57% rename from packages/offix-client/integration_test/test/conflict.test.js rename to packages/offix-client/test/integration/Conflict.test.ts index addb63531..f4cae60c0 100644 --- a/packages/offix-client/integration_test/test/conflict.test.js +++ b/packages/offix-client/test/integration/Conflict.test.ts @@ -1,12 +1,12 @@ -import { createClient } from '../../dist'; -import { TestStore } from '../utils/testStore'; -import { ToggleableNetworkStatus } from '../utils/network'; -import server from '../utils/server'; +import { createClient } from "../../types"; +import { TestStore } from "../utils/testStore"; +import { ToggleableNetworkStatus } from "../utils/network"; +import server from "../utils/server"; import { ADD_TASK, GET_TASKS, - UPDATE_TASK_CLIENT_RESOLUTION, -} from '../utils/graphql.queries'; + UPDATE_TASK_CLIENT_RESOLUTION +} from "../utils/graphql.queries"; const newNetworkStatus = (online = true) => { const networkStatus = new ToggleableNetworkStatus(); @@ -24,38 +24,39 @@ const newClient = async (clientOptions = {}) => { return await createClient(config); }; -describe('Conflicts', function () { +describe("Conflicts", function() { this.timeout(2000); const newTask = { - description: 'new', - title: 'new', + description: "new", + title: "new", version: 1, - author: 'new' + author: "new" }; - let client, networkStatus, store; + let client; + let store; - before('start server', async function () { + before("start server", async function() { await server.start(); }); - after('stop server', async function () { + after("stop server", async function() { await server.stop(); }); - beforeEach('reset server', async function () { + beforeEach("reset server", async function() { await server.reset(); }); - beforeEach('create client', async function () { - networkStatus = newNetworkStatus(false); + beforeEach("create client", async function() { + const networkStatus = newNetworkStatus(false); store = new TestStore(); client = await newClient({ networkStatus, storage: store }); }); - const createBasicConflict = async (mutation, variables1, variables2, secondClient, customConflict) => { + const createBasicConflict = async (mutation, variables1, variables2, secondClient, customConflict?) => { networkStatus.setOnline(true); const response = await client.mutate({ @@ -63,7 +64,7 @@ describe('Conflicts', function () { variables: newTask }).catch(error => { return; - }) + }); const task = response.data.createTask; @@ -81,14 +82,13 @@ describe('Conflicts', function () { const conflictListener = { conflictOccurred: () => conflict++, mergeOccurred: () => merge++ - } - + }; if (customConflict) { const customStrategy = { resolve: ({base: baseData, server: serverData, client: clientData}) => { const something = Object.assign(baseData, serverData, clientData); - something.description = 'custom'; + something.description = "custom"; return something; } }; @@ -101,8 +101,6 @@ describe('Conflicts', function () { query: GET_TASKS }); - console.log("GET TASKS", result.data.allTasks[0]); - await secondClient.query({ query: GET_TASKS }); @@ -112,21 +110,21 @@ describe('Conflicts', function () { await secondClient.offlineMutate({ mutation, variables: vars1, - returnType: 'Task' + returnType: "Task" }).catch(error => { return; - }) + }); networkStatus.setOnline(false); await client.offlineMutate({ mutation, variables: vars2, - returnType: 'Task' + returnType: "Task" }).catch(error => { return; - }) + }); networkStatus.setOnline(true); @@ -135,7 +133,7 @@ describe('Conflicts', function () { return { success, failure, conflict, merge }; }; - const createAdvancedClientConflict = async (mutation, variables1, variables2, secondClient, customConflict) => { + const createAdvancedClientConflict = async (mutation, variables1, variables2, secondClient, customConflict?) => { networkStatus.setOnline(true); const response = await client.mutate({ @@ -159,7 +157,7 @@ describe('Conflicts', function () { const conflictListener = { conflictOccurred: () => conflict++, mergeOccurred: () => merge++ - } + }; client = await newClient({ networkStatus, storage: store, offlineQueueListener: listener, conflictListener }); @@ -177,7 +175,7 @@ describe('Conflicts', function () { await secondClient.offlineMutate({ mutation, variables: vars1, - returnType: 'Task' + returnType: "Task" }); networkStatus.setOnline(false); @@ -185,20 +183,20 @@ describe('Conflicts', function () { await client.offlineMutate({ mutation, variables: vars2, - returnType: 'Task' + returnType: "Task" }).catch(error => { return; - }) + }); await client.offlineMutate({ mutation, variables: vars3, - returnType: 'Task' + returnType: "Task" }).catch(error => { return; - }) + }); networkStatus.setOnline(true); @@ -207,7 +205,7 @@ describe('Conflicts', function () { return { success, failure, conflict, merge }; }; - const createAdvancedServerConflict = async (mutation, variables1, variables2, secondClient, customConflict) => { + const createAdvancedServerConflict = async (mutation, variables1, variables2, secondClient, customConflict?) => { networkStatus.setOnline(true); const response = await client.mutate({ @@ -231,7 +229,7 @@ describe('Conflicts', function () { const conflictListener = { conflictOccurred: () => conflict++, mergeOccurred: () => merge++ - } + }; client = await newClient({ networkStatus, storage: store, offlineQueueListener: listener, conflictListener }); @@ -249,13 +247,13 @@ describe('Conflicts', function () { await secondClient.offlineMutate({ mutation, variables: vars1, - returnType: 'Task' + returnType: "Task" }); await secondClient.offlineMutate({ mutation, variables: vars2, - returnType: 'Task' + returnType: "Task" }).catch(ignore => { return; }); @@ -264,10 +262,10 @@ describe('Conflicts', function () { await client.offlineMutate({ mutation, variables: vars3, - returnType: 'Task' + returnType: "Task" }).catch(error => { return; - }) + }); networkStatus.setOnline(true); @@ -276,211 +274,207 @@ describe('Conflicts', function () { return { success, failure, conflict, merge }; }; - describe('no conflict should be thrown for mergeable data', function () { + describe("no conflict should be thrown for mergeable data", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new" }; const updatedDescription = { title: "new", description: "updated" }; - const store2 = new TestStore(); - const networkStatus = newNetworkStatus(); - const client2 = await newClient({ networkStatus, storage: store2 }); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); const { success, failure } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2); - const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); + expect(success).toBe(1); + expect(failure).toBe(0); - console.log("ALL TASKS", response.data.allTasks); - - expect(success).to.equal(1); - expect(failure).to.equal(0); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('updated'); - expect(response.data.allTasks[0].description).to.equal('updated'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("updated"); + expect(response.data.allTasks[0].description).toBe("updated"); }); }); - describe('merge should be called for mergeable conflict', function () { + describe("merge should be called for mergeable conflict", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new", author: "new" }; const updatedDescription = { title: "new", description: "updated", author: "new" }; - const store2 = new TestStore(); - const networkStatus = newNetworkStatus(); - const client2 = await newClient({ networkStatus, storage: store2 }); - const { success, failure, conflict, merge } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); + + const { + success, + failure, + conflict, + merge + } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); - expect(conflict).to.equal(0); - expect(merge).to.equal(1); - expect(success).to.equal(1); - expect(failure).to.equal(0); + expect(conflict).toBe(0); + expect(merge).toBe(1); + expect(success).toBe(1); + expect(failure).toBe(0); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('updated'); - expect(response.data.allTasks[0].description).to.equal('updated'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("updated"); + expect(response.data.allTasks[0].description).toBe("updated"); }); }); - describe('conflict should be called for unmergeable data', function () { + describe("conflict should be called for unmergeable data", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new" }; const updatedTitleAgain = { title: "another update with conflict", description: "new" }; - const networkStatus = newNetworkStatus(); - const store2 = new TestStore(); - const client2 = await newClient({ networkStatus, storage: store2 }); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); const { success, failure, conflict } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedTitleAgain, client2); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); - expect(conflict).to.equal(1); - expect(failure).to.equal(0); - expect(success).to.equal(1); + expect(conflict).toBe(1); + expect(failure).toBe(0); + expect(success).toBe(1); }); }); - describe('custom resolution strategy should override', function () { + describe("custom resolution strategy should override", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "new", description: "new", author: "new" }; const updatedDescription = { title: "updated", description: "new", author: "new" }; - const store = new TestStore(); - const networkStatus = newNetworkStatus(); const client2 = await newClient({ - networkStatus, storage: store + networkStatus: newNetworkStatus(), + storage: new TestStore() }); const { success, failure } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2, true); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); - - expect(success).to.equal(1); - expect(failure).to.equal(0); + expect(success).toBe(1); + expect(failure).toBe(0); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('updated'); - expect(response.data.allTasks[0].description).to.equal('custom'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("updated"); + expect(response.data.allTasks[0].description).toBe("custom"); }); }); - describe('default strategy should be client wins', function () { + describe("default strategy should be client wins", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new" }; const updatedBoth = { title: "client wins", description: "updated" }; - const store2 = new TestStore(); - const networkStatus = newNetworkStatus(); - const client2 = await newClient({ networkStatus, storage: store2 }); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); const { success, failure } = await createBasicConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedBoth, client2); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); + expect(success).toBe(1); + expect(failure).toBe(0); - expect(success).to.equal(1); - expect(failure).to.equal(0); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('client wins'); - expect(response.data.allTasks[0].description).to.equal('updated'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("client wins"); + expect(response.data.allTasks[0].description).toBe("updated"); }); }); - describe('multiple offline conflicts should be resolved', function () { + describe("multiple offline conflicts should be resolved", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new", author: "new" }; const updatedDescription = { title: "new", description: "updated", author: "new" }; - const store2 = new TestStore(); - const networkStatus = newNetworkStatus(); - - const client2 = await newClient({ networkStatus, storage: store2 }); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); const { success, failure } = await createAdvancedClientConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); + expect(success).toBe(2); + expect(failure).toBe(0); - expect(success).to.equal(2); - expect(failure).to.equal(0); - - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('updated'); - expect(response.data.allTasks[0].description).to.equal('updated'); - expect(response.data.allTasks[0].author).to.equal('Advanced conflict author'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("updated"); + expect(response.data.allTasks[0].description).toBe("updated"); + expect(response.data.allTasks[0].author).toBe("Advanced conflict author"); }); }); - describe('offline conflict should not be affected by multiple server edits', function () { + describe("offline conflict should not be affected by multiple server edits", function() { - it('should succeed', async function () { + it("should succeed", async function() { const updatedTitle = { title: "updated", description: "new", author: "new" }; const updatedDescription = { title: "new", description: "updated", author: "new" }; - const store2 = new TestStore(); - const networkStatus = newNetworkStatus(); + const client2 = await newClient({ + networkStatus: newNetworkStatus(), + storage: new TestStore() + }); - const client2 = await newClient({ networkStatus, storage: store2 }); const { success, failure } = await createAdvancedServerConflict(UPDATE_TASK_CLIENT_RESOLUTION, updatedTitle, updatedDescription, client2); const response = await client.query({ query: GET_TASKS, - fetchPolicy: 'network-only' + fetchPolicy: "network-only" }); - console.log("ALL TASKS", response.data.allTasks); - - expect(success).to.equal(1); - expect(failure).to.equal(0); + expect(success).toBe(1); + expect(failure).toBe(0); - expect(response.data.allTasks).to.exist; - expect(response.data.allTasks.length).to.equal(1); - expect(response.data.allTasks[0].title).to.equal('updated'); - expect(response.data.allTasks[0].description).to.equal('updated'); - expect(response.data.allTasks[0].author).to.equal('Advanced conflict author'); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("updated"); + expect(response.data.allTasks[0].description).toBe("updated"); + expect(response.data.allTasks[0].author).toBe("Advanced conflict author"); }); }); diff --git a/packages/offix-client/test/integration/Offline.test.ts b/packages/offix-client/test/integration/Offline.test.ts new file mode 100644 index 000000000..e1a4f6c23 --- /dev/null +++ b/packages/offix-client/test/integration/Offline.test.ts @@ -0,0 +1,533 @@ +import { createClient } from "../../types"; +import { createOptimisticResponse, CacheOperation } from "offix-cache"; +import { TestStore } from "../utils/testStore"; +import { ToggleableNetworkStatus } from "../utils/network"; +import server from "../utils/server"; +import { ADD_TASK, GET_TASKS, UPDATE_TASK, DELETE_TASK } from "../utils/graphql.queries"; + +function wait(time: number) { + return new Promise((resolve, reject) => { + setTimeout(resolve, time) ; + }); +} + +// TODO: error handling when server is down + +const newNetworkStatus = (online = true) => { + const networkStatus = new ToggleableNetworkStatus(); + networkStatus.setOnline(online); + return networkStatus; +}; + +const offlineMetaKey = "offline-meta-data"; + +const newClient = async (clientOptions = {}) => { + const config = { + httpUrl: "http://localhost:4000/graphql", + wsUrl: "ws://localhost:4000/graphql", + ...clientOptions + }; + + return await createClient(config); +}; + +describe("Offline mutations", function() { + + this.timeout(20000); + + const mutationsQueueName = "offline-mutation-queue"; + + const newTask = { + description: "new", + title: "new", + version: 1, + author: "new" + }; + + const optimisticOptions = { + mutation: ADD_TASK, + operationType: CacheOperation.ADD, + returnType: "Task", + variables: newTask, + updateQuery: GET_TASKS + }; + + const updatedTask = { + description: "updated", + title: "updated" + }; + + before("start server", async function() { + await server.start(); + }); + + after("stop server", async function() { + await server.stop(); + }); + + beforeEach("reset server", async function() { + await server.reset(); + }); + + async function isQueueEmpty() { + const store = await store.getItem(offlineMetaKey); + return store.length === 0; + } + + describe("offline mutations", function() { + + it("mutations are queued when offline", async function() { + + const client = await newClient({ + networkStatus: newNetworkStatus(false), + storage: new TestStore() + }); + + const mutationOptions = { + mutation: ADD_TASK, + variables: newTask + }; + await client.offlineMutate(mutationOptions).catch(err => { + // ignore + }); + + const queue = client.queue.queue; + expect(queue.length).toBe(1); + const op = queue[0].operation.op; + expect(op.mutation).toEqual(mutationOptions.mutation); + expect(op.variables).toEqual(mutationOptions.variables); + }); + + it("mutations are persisted while offline", async function() { + + const client = await newClient({ + networkStatus: newNetworkStatus(false), + storage: new TestStore() + }); + + const mutationOptions = { + mutation: ADD_TASK, + variables: newTask + }; + + await client.offlineMutate(mutationOptions).catch(err => { + // ignore + }); + + const store = client.queue.store; + const storeData = await store.getOfflineData(); + + expect(storeData.length).toBe(1); + + const op = storeData[0].operation.op; + expect(op.mutation).toEqual(mutationOptions.mutation); + expect(op.variables).toEqual(mutationOptions.variables); + }); + + it("offlineMutate throws an offline error, we can get result when coming back online", async function(done) { + + const client = await newClient({ + networkStatus: newNetworkStatus(false), + storage: new TestStore() + }); + + client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask + }).catch((err) => { + const promise = err.offlineMutatePromise; + networkStatus.setOnline(true); // go online + promise.then((response) => { + expect(response.data.createTask).toBeDefined(); + expect(response.data.createTask.title).toBe(newTask.title); + expect(response.data.createTask.description).toBe(newTask.description); + done(); + }); + }); + }); + + it("queues multiple mutations while offline", async function() { + const networkStatus = newNetworkStatus(true); + + const client = await newClient({ + networkStatus, + storage: new TestStore() + }); + + const response = await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask + }); + + const task = response.data.createTask; + + networkStatus.setOnline(false); + + const variables = { ...updatedTask, id: task.id, version: task.version }; + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + variables + }); + } catch (ignore) { + // ingore + } + + try { + await client.offlineMutate({ + mutation: DELETE_TASK, + variables: { id: task.id } + }); + } catch (ignore) { + // ignore + } + + const queue = client.queue.queue; + expect(queue.length).toBe(2); + expect(queue[0].operation.op.context.operationName).toBe("updateTask"); + expect(queue[1].operation.op.context.operationName).toBe("deleteTask"); + }); + + it("Handles multiple mutations on the same object in queue/store", async function() { + const networkStatus = newNetworkStatus(true); + + const client = await newClient({ + networkStatus, + storage: new TestStore() + }); + + const result = await client.mutate({ + mutation: ADD_TASK, + variables: newTask + }); + + networkStatus.setOnline(false); + + const task = result.data.createTask; + const variables = { ...task, ...updatedTask }; + + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + variables + }); + } catch (ignore) { + // ignore + } + try { + await client.offlineMutate({ + mutation: DELETE_TASK, + variables: { id: task.id } + }); + } catch (ignore) { + // ignore + } + + networkStatus.setOnline(true); + + await new Promise(resolve => setTimeout(resolve, 300)); + + const response = await client.query({ + query: GET_TASKS, + fetchPolicy: "network-only" + }); + + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(0); + }); + + it("can create new items and subsequently mutate them while offline, async function ()", async () => { + const networkStatus = newNetworkStatus(false); + + const client = await newClient({ + networkStatus, + storage: new TestStore() + }); + + let task; + const optimisticResponse = createOptimisticResponse(optimisticOptions); + try { + await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask, + optimisticResponse, + update: (_, { data: { createTask } }) => task = createTask + }); + } catch (ignore) { + // ignore + } + + const variables = { ...updatedTask, id: task.id, version: task.version }; + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + variables + }); + + } catch (ignore) { + // ignore + } + networkStatus.setOnline(true); + + await new Promise(resolve => setTimeout(resolve, 300)); + + const response = await client.query({ + query: GET_TASKS, + fetchPolicy: "network-only" + }); + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe(updatedTask.title); + }); + + it("reinitialized client will replay operations from the offline storage (simulates an app restart)", async function() { + let networkStatus = newNetworkStatus(false); + const store = new TestStore(); + + const client = await newClient({ + networkStatus, + storage: store + }); + + let task; + try { + await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask, + optimisticResponse: createOptimisticResponse(optimisticOptions), + update: (_, { data: { createTask } }) => task = createTask + }); + } catch (ignore) { + // ignore + } + + const variables = { ...updatedTask, id: task.id, version: task.version }; + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + variables + }); + } catch (ignore) { + // ignore + } + + networkStatus = newNetworkStatus(); + client = await newClient({ networkStatus, storage: store, mutationsQueueName }); + + await new Promise(resolve => setTimeout(resolve, 300)); + + const response = await client.query({ + query: GET_TASKS, + fetchPolicy: "network-only" + }); + + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe(updatedTask.title); + }); + + it("it can handle multiple offline mutations on an existing object and replay them successfully", async function() { + + const networkStatus = newNetworkStatus(true); + const store = new TestStore(); + + const client = await newClient({ + networkStatus, + storage: store + }); + + const result = await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask, + returnType: "Task", + optimisticResponse: createOptimisticResponse(optimisticOptions) + }); + + const task = result.data.createTask; + + networkStatus.setOnline(false); + + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + returnType: "Task", + variables: { title: "update1", description: "merge", id: task.id, version: task.version } + }); + } catch (err) { + // ignore + } + + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + returnType: "Task", + variables: { title: "update2", description: "merge", id: task.id, version: task.version } + }); + } catch (err) { + // ignore + } + + networkStatus.setOnline(true); + + await wait(1000); + + const response = await client.query({ + query: GET_TASKS, + fetchPolicy: "network-only" + }); + + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("update2"); + }); + + }); + + describe("do not merge offline mutations", async function() { + + it("should succeed", async function() { + let task; + const networkStatus = newNetworkStatus(false); + const store = new TestStore(); + + const client = await newClient({ + networkStatus, + storage: store + }); + + networkStatus.setOnline(true); + + let response = await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask, + optimisticResponse: createOptimisticResponse(optimisticOptions) + }); + + task = response.data.createTask; + + networkStatus.setOnline(false); + + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + returnType: "Task", + variables: { title: "nomerge1", description: "nomerge", id: task.id, version: task.version } + }); + } catch (ignore) { + // ignore + } + + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + returnType: "Task", + variables: { title: "nomerge2", description: "nomerge", id: task.id, version: task.version } + }); + } catch (ignore) { + // ignore + } + + const offlineData = await client.queue.store.getOfflineData(); + + expect(offlineData.length).toBe(2); + + expect(offlineData[0]).toBeDefined(); + expect(offlineData[1]).toBeDefined(); + expect(offlineData[0].operation.op.variables.title).toBe("nomerge1"); + expect(offlineData[1].operation.op.variables.title).toBe("nomerge2"); + + networkStatus.setOnline(true); + + await wait(300); + + const response = await client.query({ + query: GET_TASKS, + fetchPolicy: "network-only" + }); + + expect(response.data.allTasks).toBeDefined(); + expect(response.data.allTasks.length).toBe(1); + expect(response.data.allTasks[0].title).toBe("nomerge2"); + }); + }); + + describe("ensure offline params are added to offline object ", function() { + + it("should succeed", async function() { + let task; + const networkStatus = newNetworkStatus(false); + const store = new TestStore(); + + const client = await newClient({ + networkStatus, + storage: store + }); + networkStatus.setOnline(true); + + const response = await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask, + optimisticResponse: createOptimisticResponse(optimisticOptions) + }); + + task = response.data.createTask; + + networkStatus.setOnline(false); + + const variables = { title: "nomerge1", description: "nomerge", id: task.id, version: task.version }; + try { + await client.offlineMutate({ + mutation: UPDATE_TASK, + variables, + idField: "someOtherField", + returnType: "ADifferentType" + }); + } catch (ignore) { + // ignore + } + + const offlineData = await client.queue.store.getOfflineData(); + const op = offlineData[0].operation.op; + expect(op.context.returnType).toBe("ADifferentType"); + expect(op.context.idField).toBe("someOtherField"); + }); + }); + + describe("notify about offline changes", function() { + it("should succeed", async function() { + const networkStatus = newNetworkStatus(false); + const store = new TestStore(); + + let offlineOps = 0; + let cleared = 0; + + const listener = { + onOperationEnqueued: () => offlineOps++, + queueCleared: () => cleared++ + }; + + const client = await newClient({ + networkStatus, + storage: store, + offlineQueueListener: listener + }); + + try { + await client.offlineMutate({ + mutation: ADD_TASK, + variables: newTask + }); + } catch (ignore) { + // ignore + } + + expect(offlineOps).toBe(1); + + networkStatus.setOnline(true); + + await new Promise(resolve => setTimeout(resolve, 300)); + + expect(cleared).toBe(1); + }); + }); +}); diff --git a/packages/offix-client/test/server/graphql.ts b/packages/offix-client/test/server/graphql.ts new file mode 100644 index 000000000..7784b74f6 --- /dev/null +++ b/packages/offix-client/test/server/graphql.ts @@ -0,0 +1,31 @@ +import express from "express"; +import { VoyagerServer } from "@aerogear/voyager-server"; +import http from "http"; +import { SubscriptionServer } from "subscriptions-transport-ws"; +import { execute, subscribe } from "graphql"; + +import { typeDefs, resolvers } from "./schema"; + +const PORT = 4000; + +export function startServer() { + const app = express(); + + const apolloServer = VoyagerServer({ typeDefs, resolvers }); + const httpServer = http.createServer(app); + apolloServer.applyMiddleware({ app }); + + return new Promise(resolve => { + httpServer.listen({ port: PORT }, async () => { + const subscriptionServer = new SubscriptionServer({ + execute, + subscribe, + schema: apolloServer.schema + }, { + server: httpServer, + path: "/graphql" + }); + resolve(httpServer); + }); + }); +} diff --git a/packages/offix-client/test/server/index.ts b/packages/offix-client/test/server/index.ts new file mode 100644 index 000000000..e7aac9973 --- /dev/null +++ b/packages/offix-client/test/server/index.ts @@ -0,0 +1,29 @@ +import express from "express"; +import cors from "cors"; + +import { startServer } from "./graphql"; +import { resetData } from "./schema"; + +const app = express(); +app.use(cors()); + +const PORT = 4001; + +let graphqlServer; + +app.post("/start", async (_, res) => { + graphqlServer = await startServer(); + res.sendStatus(200); +}); +app.post("/stop", (_, res) => { + if (graphqlServer) { + graphqlServer.close(); + } + res.sendStatus(200); +}); +app.post("/reset", (_, res) => { + resetData(); + res.sendStatus(200); +}); + +app.listen(PORT, () => console.info(`Server listening on port ${PORT}!`)); diff --git a/packages/offix-client/integration_test/server/schema.js b/packages/offix-client/test/server/schema.ts similarity index 80% rename from packages/offix-client/integration_test/server/schema.js rename to packages/offix-client/test/server/schema.ts index 97faeb899..6d5d7c2a1 100644 --- a/packages/offix-client/integration_test/server/schema.js +++ b/packages/offix-client/test/server/schema.ts @@ -1,7 +1,7 @@ -const { gql } = require('@aerogear/voyager-server') -const { conflictHandler } = require('offix-server-conflicts') -const { PubSub } = require('graphql-subscriptions'); -const fs = require('fs'); +import { gql } from "@aerogear/voyager-server"; +import { conflictHandler } from "offix-server-conflicts"; +import { PubSub } from "graphql-subscriptions"; +import fs from "fs"; const pubSub = new PubSub(); @@ -37,7 +37,7 @@ type File { mimetype: String! encoding: String! } -` +`; let id = 0; let data = []; @@ -52,7 +52,6 @@ const resetData = () => { const resolvers = { Query: { allTasks: () => { - console.log('all: ', data); return data; }, getTask: (_, args) => { @@ -62,33 +61,30 @@ const resolvers = { return data.find(item => item.title === args.title); }, uploads: () => { - return files - }, + return files; + } }, Mutation: { createTask: (_, args) => { - console.log('create: ', args); const newTask = { ...args, id: (id++).toString(), version: 1 }; data.push(newTask); - pubSub.publish('taskCreated', { + pubSub.publish("taskCreated", { taskCreated: newTask }); return newTask; }, updateTask: async (_, args) => { - console.log('update: ', args); const index = data.findIndex(item => item.id === args.id); - const conflict = conflictHandler.checkForConflict(data[index], args) - if(conflict){ + const conflict = conflictHandler.checkForConflict(data[index], args); + if (conflict) { throw conflict; } data[index] = {...(data[index]), ...args}; return data[index]; }, deleteTask: (_, args) => { - console.log('delete: ', args); const index = data.findIndex(item => item.id === args.id); data.splice(index, 1); return args.id; @@ -109,13 +105,13 @@ const resolvers = { }, Subscription: { taskCreated: { - subscribe: () => pubSub.asyncIterator('taskCreated') + subscribe: () => pubSub.asyncIterator("taskCreated") } - }, -} + } +}; -module.exports = { +export { typeDefs, resolvers, resetData -} +}; diff --git a/packages/offix-client/integration_test/utils/graphql.queries.js b/packages/offix-client/test/utils/graphql.queries.ts similarity index 98% rename from packages/offix-client/integration_test/utils/graphql.queries.js rename to packages/offix-client/test/utils/graphql.queries.ts index 55814c9f6..56c1d23f8 100644 --- a/packages/offix-client/integration_test/utils/graphql.queries.js +++ b/packages/offix-client/test/utils/graphql.queries.ts @@ -1,4 +1,4 @@ -import gql from 'graphql-tag'; +import gql from "graphql-tag"; export const ADD_TASK = gql` mutation createTask($description: String!, $title: String!, $author: String ) { @@ -33,7 +33,7 @@ export const GET_TASK = gql` version } } -` +`; export const FIND_TASK_BY_TITLE = gql` query findTaskByTitle($title: String!) { diff --git a/packages/offix-client/integration_test/utils/mocha.js b/packages/offix-client/test/utils/mocha.ts similarity index 98% rename from packages/offix-client/integration_test/utils/mocha.js rename to packages/offix-client/test/utils/mocha.ts index 6d7fa156a..ee1ba450b 100644 --- a/packages/offix-client/integration_test/utils/mocha.js +++ b/packages/offix-client/test/utils/mocha.ts @@ -4,7 +4,7 @@ export const skipRestOnFail = () => { this.skip(); } }); - + afterEach(function() { if (this.currentTest.state === "failed") { this.currentTest.parent.failedTests = true; diff --git a/packages/offix-client/test/utils/network.ts b/packages/offix-client/test/utils/network.ts new file mode 100644 index 000000000..561c96d7b --- /dev/null +++ b/packages/offix-client/test/utils/network.ts @@ -0,0 +1,35 @@ +const onLineGetter = window.navigator.__lookupGetter__("onLine"); + +export const goOffLine = () => { + window.navigator.__defineGetter__("onLine", () => false); +}; + +export const goOnLine = () => { + window.navigator.__defineGetter__("onLine", onLineGetter); +}; + +export class ToggleableNetworkStatus { + public online: any; + public callbacks: any; + + constructor() { + this.online = true; + this.callbacks = []; + } + + public onStatusChangeListener(callback) { + this.callbacks.push(callback); + } + + public isOffline() { + const online = this.online; + return new Promise(resolve => resolve(!online)); + } + + public setOnline(online) { + this.online = online; + for (const callback of this.callbacks) { + callback.onStatusChange({ online }); + } + } +} diff --git a/packages/offix-client/test/utils/server.ts b/packages/offix-client/test/utils/server.ts new file mode 100644 index 000000000..147a02f94 --- /dev/null +++ b/packages/offix-client/test/utils/server.ts @@ -0,0 +1,21 @@ +function manage(cmd: string) { + return fetch(`http://localhost:4001/${cmd}`, { method: "POST" }); +} + +function start() { + return manage("start"); +} + +function stop() { + return manage("stop"); +} + +function reset() { + return manage("reset"); +} + +export default { + start, + stop, + reset +}; diff --git a/packages/offix-client/integration_test/utils/testApolloLink.js b/packages/offix-client/test/utils/testApolloLink.ts similarity index 68% rename from packages/offix-client/integration_test/utils/testApolloLink.js rename to packages/offix-client/test/utils/testApolloLink.ts index 8c43fe188..af1cc9c18 100644 --- a/packages/offix-client/integration_test/utils/testApolloLink.js +++ b/packages/offix-client/test/utils/testApolloLink.ts @@ -1,4 +1,4 @@ -import { ApolloLink } from 'apollo-link'; +import { ApolloLink } from "apollo-link"; const testLink = { link: new ApolloLink((operation, forward) => { @@ -8,4 +8,4 @@ const testLink = { counter: 0 }; -export default testLink; \ No newline at end of file +export default testLink; diff --git a/packages/offix-client/integration_test/utils/testStore.js b/packages/offix-client/test/utils/testStore.ts similarity index 59% rename from packages/offix-client/integration_test/utils/testStore.js rename to packages/offix-client/test/utils/testStore.ts index 17492f69f..a868758ef 100644 --- a/packages/offix-client/integration_test/utils/testStore.js +++ b/packages/offix-client/test/utils/testStore.ts @@ -1,25 +1,27 @@ export class TestStore { + public data: any; + constructor() { this.data = {}; } - getItem(key) { + public getItem(key) { return new Promise((resolve, reject) => { - resolve(this.data[key]) - }) + resolve(this.data[key]); + }); } - setItem(key, data) { + public setItem(key, data) { return new Promise((resolve, reject) => { this.data[key] = data; - resolve() - }) + resolve(); + }); } - removeItem(key) { + public removeItem(key) { return new Promise((resolve, reject) => { delete this.data[key]; - resolve() - }) + resolve(); + }); } } diff --git a/packages/offix-client/integration_test/utils/timeout.js b/packages/offix-client/test/utils/timeout.ts similarity index 77% rename from packages/offix-client/integration_test/utils/timeout.js rename to packages/offix-client/test/utils/timeout.ts index f8b1c5f4c..ea8fc22ac 100644 --- a/packages/offix-client/integration_test/utils/timeout.js +++ b/packages/offix-client/test/utils/timeout.ts @@ -2,4 +2,4 @@ function timeout(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } - export default timeout; \ No newline at end of file + export default timeout; diff --git a/packages/offix-client/integration_test/utils/waitFor.js b/packages/offix-client/test/utils/waitFor.ts similarity index 86% rename from packages/offix-client/integration_test/utils/waitFor.js rename to packages/offix-client/test/utils/waitFor.ts index 39016b625..f5d9b3905 100644 --- a/packages/offix-client/integration_test/utils/waitFor.js +++ b/packages/offix-client/test/utils/waitFor.ts @@ -4,4 +4,4 @@ const waitFor = async (conditionFn, time = 0) => { } }; -export default waitFor; \ No newline at end of file +export default waitFor;