Parcourir la source

added testing

Tomi Cvetic il y a 6 ans
Parent
commit
d079130c54

+ 21 - 0
backend/__tests__/src/__mocks__/fs.js

@@ -0,0 +1,21 @@
+const path = require('path')
+const fs = jest.genMockFromModule('fs')
+
+let mockFiles = {}
+function __setMockFiles (newMockFiles) {
+  mockFiles = newMockFiles
+}
+
+function readdir (dirPath, callback) {
+  callback(null, mockFiles)
+}
+
+function stat (file, callback) {
+  callback(null, { bugu: 12 })
+}
+
+fs.__setMockFiles = __setMockFiles
+fs.readdir = readdir
+fs.stat = stat
+
+module.exports = fs

+ 24 - 0
backend/__tests__/src/interfaces.spec.js

@@ -0,0 +1,24 @@
+jest.mock('fs')
+const { resolvers, typeDefs } = require('../../src/interfaces')
+
+describe('interfaces module', () => {
+  const MOCK_FILES = ['serial_worker.py', 'usbtmc_worker.py']
+
+  beforeEach(() => {
+    require('fs').__setMockFiles(MOCK_FILES)
+  })
+  it('exports resolvers and typeDefs', () => {
+    expect(resolvers).toBeDefined()
+    expect(typeDefs).toBeDefined()
+  })
+  it('resolves queries and mutations', () => {
+    expect(resolvers).toHaveProperty('Query'),
+    expect(resolvers).toHaveProperty('Mutation')
+  })
+
+  const { Query, Mutation, Interface, Connection } = resolvers
+  it('finds interfaces', async () => {
+    const resp = await Query.interfaces()
+    expect(resp).toBe([])
+  })
+})

+ 42 - 0
backend/__tests__/src/utils.spec.js

@@ -0,0 +1,42 @@
+const { serializeError } = require('../../src/utils')
+
+describe('utils module', () => {
+  it('exports the serializeError function', () => {
+    expect(serializeError).toBeDefined()
+  })
+  it('is a function', () => {
+    expect(serializeError).toBeInstanceOf(Function)
+  })
+
+  it('throws an error on wrong argument type', () => {
+    expect(() => serializeError(1)).toThrow(
+      'Can only handle instances of "Error". Found instance of "number"'
+    )
+    expect(() => serializeError('one')).toThrow(
+      'Can only handle instances of "Error". Found instance of "string"'
+    )
+    expect(() => serializeError([1, 'one'])).toThrow(
+      'Can only handle instances of "Error". Found instance of "object"'
+    )
+    expect(() => serializeError({ one: 1 })).toThrow(
+      'Can only handle instances of "Error". Found instance of "object"'
+    )
+  })
+
+  it('serializes an error correctly', () => {
+    const message = 'Just a test, relax...'
+    const testError = new Error(message)
+    const serializedError = serializeError(testError)
+    expect(serializedError).toBeInstanceOf(Object)
+    expect(serializedError).toHaveProperty('error')
+    const { error } = serializedError
+    expect(error).toEqual(expect.stringContaining('name'))
+    expect(() => JSON.parse(error)).not.toThrow()
+    const parsedError = JSON.parse(error)
+    expect(parsedError).toMatchObject({
+      name: 'Error',
+      message,
+      stack: testError.stack
+    })
+  })
+})

Fichier diff supprimé car celui-ci est trop grand
+ 803 - 3
backend/package-lock.json


+ 14 - 2
backend/package.json

@@ -26,16 +26,28 @@
     "standard": "^12.0.1"
   },
   "devDependencies": {
+    "babel-jest": "^24.8.0",
+    "jest": "^24.8.0",
+    "jest-transform-graphql": "^2.1.0",
     "graphql-cli": "^3.0.11",
     "nodemon": "^1.18.11"
   },
   "scripts": {
     "start": "nodemon -e js,graphql -x node index.js",
     "dev": "nodemon -e js,graphql -x node --inspect index.js",
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "test": "NODE_ENV=test jest --watch",
     "deploy": "prisma deploy"
   },
+  "jest": {
+    "testPathIgnorePatterns": [
+      "<rootDir>/node_modules/"
+    ],
+    "transform": {
+      "\\.(gql|graphql)$": "jest-transform-graphql",
+      ".*": "babel-jest"
+    }
+  },
   "keywords": [],
   "author": "",
   "license": "ISC"
-}
+}

+ 76 - 98
backend/src/interfaces.js

@@ -30,8 +30,8 @@ const state = {
   lastScan: {
     workers: Date.now() - 100000,
     ports: Date.now() - 100000,
-    connections: Date.now() - 100000,
-  },
+    connections: Date.now() - 100000
+  }
 }
 
 const typeDefs = `
@@ -79,7 +79,7 @@ const typeDefs = `
   type Connection {
     id: ID!
     port: Port!
-    workerProcess: WorkerProcess
+    workerProcess: WorkerProcess!
   }
 
   input ConnectionCommand {
@@ -90,8 +90,8 @@ const typeDefs = `
 
   extend type Query {
     interfaces: [Interface]!
-    interface(id: ID, interfaceName: String): Interface!
-    ports(interfaceName: String, force: Boolean): [Port]!
+    interface(id: ID!): Interface!
+    ports: [Port]!
     port(id: ID!): Port!
     connections: [Connection]!
     connection(id: ID!): Connection!
@@ -108,7 +108,7 @@ const typeDefs = `
 /**
  * INTERFACE SECTION
  */
-async function findInterfaces() {
+async function findInterfaces () {
   // 1. Don't check more frequently than once per second.
   // if (state.lastScan.workers + 1000 > Date.now()) return null
   // state.lastScan.workers = Date.now()
@@ -131,8 +131,9 @@ async function findInterfaces() {
     )
     if (foundInterface) {
       // b. If it was modified, save the modification time.
-      if (foundInterface.workerScript.mtime < mtime)
+      if (foundInterface.workerScript.mtime < mtime) {
         foundInterface.workerScript.updated = mtime
+      }
       return
     }
     // c. Spawn a new worker connection.
@@ -145,30 +146,19 @@ async function findInterfaces() {
       interfaceName,
       workerScript,
       workerProcess,
-      ports: [],
-      connections: [],
-      options: await options(workerProcess),
+      options: await options(workerProcess)
     })
   })
   await Promise.all(workerPromises)
 }
 
-async function interfaces(parent, args, context, info) {
+async function interfaces (parent, args, context, info) {
   await findInterfaces()
-  return state.interfaces.map(iface => ({
-    ...iface,
-    ports: ports(iface.interfaceName, args, context, info),
-    // Serialize worker process
-    workerProcess: workerProcess(iface.workerProcess, args, context, info),
-  }))
+  return state.interfaces
 }
 
-async function interface(parent, args, context, info) {
+async function iface (parent, { id }, context, info) {
   await findInterfaces()
-  const { id, interfaceName } = args
-  if (!id && !interfaceName) {
-    throw new Error('Either id or interfaceName needs to be provided!')
-  }
   const iface = state.interfaces.find(
     iface => iface.id === id || iface.interfaceName === interfaceName
   )
@@ -177,41 +167,41 @@ async function interface(parent, args, context, info) {
   }
   return {
     ...iface,
-    workerProcess: workerProcess(iface.workerProcess, args, context, info),
+    workerProcess
   }
 }
 
-async function options(workerProcess) {
+async function options (workerProcess) {
   const { data, error } = await workerProcess.send({
-    type: 'options',
+    type: 'options'
   })
   if (error) throw new Error(error)
   return data
 }
 
-function workerProcess(parent, args, ctx, info) {
+function workerProcess (parent, args, context, info) {
   const {
     killed,
     exitCode,
     signalCode,
     spawnargs,
     spawnfile,
-    pid,
-  } = parent.pythonShell
+    pid
+  } = parent.workerProcess.pythonShell
   return {
     pid,
     killed,
     exitCode,
     signalCode,
     spawnfile,
-    spawnargs,
+    spawnargs
   }
 }
 
 /**
  * PORTS SECTION
  */
-async function findPorts() {
+async function findPorts () {
   // 1. Make sure, workers are updated.
   await findInterfaces()
 
@@ -222,8 +212,8 @@ async function findPorts() {
   // 3. Loop through all workers to find available ports.
   const portsPromises = state.interfaces.map(async iface => {
     // a) Ask interface for ports.
-    const { data, error, pythonError } = await iface.workerProcess.send({
-      type: 'ports',
+    const { data, error } = await iface.workerProcess.send({
+      type: 'ports'
     })
     if (error) throw new Error(error)
     // b) Add all ports that are not in the list.
@@ -232,35 +222,31 @@ async function findPorts() {
       if (state.ports.find(port => port.id === id)) return null
       const newPort = {
         id,
+        interface: iface,
         interfaceName: iface.interfaceName,
         host: HOST,
-        ...port,
+        ...port
       }
       state.ports.push(newPort)
-      iface.ports.push(newPort)
     })
   })
   await Promise.all(portsPromises)
 }
 
-async function ports(parent, args, ctx, info) {
+async function ports (parent, args, context, info) {
   await findPorts()
-  const { interfaceName } = args
-  const ifName = interfaceName || parent
 
-  if (ifName) {
-    const iface = state.interfaces.find(iface => iface.interfaceName === ifName)
-    if (!iface) throw new Error(`Interface ${ifName} not found.`)
-    return iface.ports
+  if (parent) {
+    return state.ports.filter(
+      port => port.interfaceName === parent.interfaceName
+    )
   } else {
     return state.ports
   }
 }
 
-async function port(parent, args, ctx, info) {
+async function port (parent, { id }, context, info) {
   await findPorts()
-  const { id } = args
-  if (!id) throw new Error('Need an id.')
   const port = state.ports.find(port => port.id === id)
   return port
 }
@@ -268,102 +254,86 @@ async function port(parent, args, ctx, info) {
 /**
  * CONNECTION SECTION
  */
-async function connect(parent, args, ctx, info) {
+async function connect (parent, { portId }, ctx, info) {
   await findPorts()
-  const { portId } = args
   const port = state.ports.find(port => port.id === portId)
   if (!port) throw new Error(`Port ${portId} not found`)
-  const iface = state.interfaces.find(
-    iface => iface.interfaceName === port.interfaceName
-  )
-  const id = md5(iface.interfaceName + iface.device)
-  if (iface.connections.find(connection => connection.id === id)) {
+
+  const id = md5(port.interfaceName + port.device)
+  if (state.connections.find(connection => connection.id === id)) {
     throw new Error('already connected.')
   }
-  const pythonWorker = new PythonWorker(iface.workerScript)
-  const spawnData = await pythonWorker.spawn()
+  const workerProcess = new PythonWorker(port.interface.workerScript)
+  const spawnData = await workerProcess.spawn()
   if (spawnData.error) throw new Error(spawnData.error)
   const connection = {
     id,
     port,
-    workerProcess: pythonWorker,
+    workerProcess
   }
-  const connectionData = await connection.workerProcess.send({
+  const connectionData = await workerProcess.send({
     type: 'connect',
-    device: port.device,
+    device: port.device
   })
   if (connectionData.error) throw new Error(connectionData.error)
-  iface.connections.push(connection)
   state.connections.push(connection)
-  return {
-    ...connection,
-    workerProcess: workerProcess(pythonWorker),
-  }
+  return connection
 }
 
-async function connections(parent, args, ctx, info) {
+async function connections (parent, args, ctx, info) {
   if (parent) {
-    const iface = state.interfaces.find(iface => iface.interfaceName === parent)
-    return iface.connections.map(connection => {
-      return {
-        ...connection,
-        workerProcess: workerProcess(connection.workerProcess),
-      }
-    })
+    return state.connections.filter(
+      connection => connection.port.interfaceName === parent.interfaceName
+    )
   } else {
-    return state.connections.map(connection => {
-      return {
-        ...connection,
-        workerProcess: workerProcess(connection.workerProcess),
-      }
-    })
+    return state.connections
   }
 }
 
-async function connection(parent, args, context, info) {
-  const connection = state.connections.find(
-    connection => connection.id === args.id
-  )
-  return {
-    ...connection,
-    workerProcess: workerProcess(connection.workerProcess),
-  }
+async function connection (parent, { id }, context, info) {
+  const connection = state.connections.find(connection => connection.id === id)
+  return connection
 }
 
-async function sendCommand(parent, args, ctx, info) {
-  const { connectionId, command } = args
+async function sendCommand (parent, { connectionId, command }, ctx, info) {
   const connection = state.connections.find(
     connection => connection.id === connectionId
   )
+  if (!connection) throw new Error('Connection not found.')
   const { data, error } = await connection.workerProcess.send({ ...command })
   if (error) throw new Error(JSON.stringify(error))
-  return data.response
+  if (!data.response) {
+    return JSON.stringify(data)
+  } else {
+    return data.response
+  }
 }
 
-// TODO Also find connections in interfaces.
-async function endConnection(parent, args, ctx, info) {
-  const { connectionId } = args
+async function endConnection (parent, { connectionId }, ctx, info) {
   const connectionIndex = state.connections.findIndex(
     connection => connection.id === connectionId
   )
+  if (connectionIndex < 0) throw new Error('Connection not found.')
   const connection = state.connections[connectionIndex]
-  const iface = state.interfaces.find(
-    iface => (iface.interfaceName = connection.interfaceName)
-  )
+
   const { data, error } = await connection.workerProcess.end()
   if (error) throw new Error(JSON.stringify(error))
+  if (data) throw new Error(`Process ended with exit code ${data}`)
   state.connections.splice(connectionIndex, 1)
   return connection
 }
 
-async function killConnection(parent, args, ctx, info) {
-  const { connectionId } = args
+async function killConnection (parent, { connectionId, signal = 9 }, ctx, info) {
   const connectionIndex = state.connections.findIndex(
     connection => connection.id === connectionId
   )
+  if (connectionIndex < 0) throw new Error('Connection not found.')
   const connection = state.connections[connectionIndex]
-  const { data, error } = await connection.workerProcess.kill()
+
+  const { data, error } = await connection.workerProcess.kill(signal)
+  console.log({ data, error })
   if (error) throw new Error(JSON.stringify(error))
+  if (data) throw new Error(`Received code ${data}`)
   state.connections.splice(connectionIndex, 1)
   return connection
 }
@@ -371,18 +341,26 @@ async function killConnection(parent, args, ctx, info) {
 const resolvers = {
   Query: {
     interfaces,
-    interface,
+    interface: iface,
     ports,
     port,
     connections,
-    connection,
+    connection
   },
   Mutation: {
     connect,
     sendCommand,
     endConnection,
-    killConnection,
+    killConnection
   },
+  Interface: {
+    ports,
+    connections,
+    workerProcess
+  },
+  Connection: {
+    workerProcess
+  }
 }
 
 module.exports = { typeDefs, resolvers }

+ 9 - 3
backend/src/pythonWorker.js

@@ -40,6 +40,7 @@ function PythonWorker (workerScript, shellOptions) {
     if (!this.isProcessRunning) return { error: 'Process not running' }
 
     return this.transaction(command => {
+      console.log('[STDIN]', command)
       this.pythonShell.stdin.write(
         // Write the command as a JSON object, end with a newline!
         JSON.stringify(command) + '\n'
@@ -57,18 +58,23 @@ function PythonWorker (workerScript, shellOptions) {
       )
       this.pythonShell.stdout.on('data', message => {
         // The python worker returns JSON {data, error}
-        this.data.push(JSON.parse(message))
+        const parsed = JSON.parse(message)
+        console.log('[STDOUT]', parsed)
+        this.data.push(parsed)
         this.pythonLock.release()
       })
       this.pythonShell.stderr.on('data', error => {
+        console.log('[STDERR]', error)
         this.error.push({ error })
         this.pythonLock.release()
       })
-      this.pythonShell.on('close', error => {
-        this.error.push({ error })
+      this.pythonShell.on('close', exitCode => {
+        console.log('[CLOSE]', exitCode)
+        this.data.push({ data: exitCode })
         this.pythonLock.release()
       })
       this.pythonShell.on('error', error => {
+        console.log('[ERROR]', error)
         this.error.push({ error })
         this.pythonLock.release()
       })

+ 4 - 2
backend/src/python_workers/test_worker.py

@@ -112,8 +112,8 @@ for line in sys.stdin:
             data = TestWorker.ports()
         elif command['type'] == 'options':
             data = TestWorker.OPTIONS
-        elif command['type'] == 'comands':
-            data = [func for func in dir(TestWorker) if callable(getattr(TestWorker, func))]
+        elif command['type'] == 'commands':
+            data = [func for func in dir(TestWorker) if callable(getattr(TestWorker, func)) and not func.startswith('__')]
         elif command['type'] == 'connect':
             options = command['options'] if 'options' in command else {}
             data = worker.connect(command['device'], **options)
@@ -123,6 +123,8 @@ for line in sys.stdin:
             data = worker.write(command['data'])
         elif command['type'] == 'ask':
             data = worker.ask(command['data'])
+        else:
+            raise Exception('Command type "{}" is not supported.'.format(command['type']))
         print(json.dumps({"data": data}), flush=True)
     except Exception as error:
         print(handle_exception(error), flush=True)

+ 5 - 0
backend/src/utils.js

@@ -1,4 +1,9 @@
 function serializeError (error) {
+  if (!(error instanceof Error)) {
+    throw new Error(
+      `Can only handle instances of "Error". Found instance of "${typeof error}"`
+    )
+  }
   return {
     error: JSON.stringify({
       name: error.name,

+ 10 - 0
frontend/__tests__/lib/localState.spec.js

@@ -0,0 +1,10 @@
+import * as dataImport from '../../lib/localState'
+
+describe('localState module', () => {
+  it('provides required exports', () => {
+    expect(dataImport).toHaveProperty('resolvers')
+    expect(dataImport).toHaveProperty('typeDefs')
+    expect(dataImport).toHaveProperty('initialState')
+    expect(dataImport.initialState).toHaveProperty('data')
+  })
+})

+ 16 - 0
frontend/__tests__/lib/withApollo.spec.js

@@ -0,0 +1,16 @@
+import withApollo from '../../lib/withApollo'
+
+const App = props => <div>I have nothing to say.</div>
+
+describe('withApollo module', () => {
+  it('is a function', () => {
+    expect(withApollo).toBeInstanceOf(Function)
+  })
+  xit('decorates a component', () => {
+    const DecoratedApp = withApollo(App)
+    const MyApp = <DecoratedApp />
+    expect(MyApp).toHaveProperty('props')
+    expect(MyApp).toHaveProperty('_owner')
+    expect(MyApp).toHaveProperty('_store')
+  })
+})

+ 16 - 0
frontend/__tests__/sample.test.js

@@ -0,0 +1,16 @@
+describe('sample test 101', () => {
+  it('works as expected', () => {
+    const age = 100
+    expect(1).toEqual(1)
+    expect(age).toEqual(100)
+  })
+
+  it('handles ranges just fine', () => {
+    const age = 200
+    expect(age).toBeGreaterThan(100)
+  })
+
+  fit('nothing else', () => {
+    expect(2).toEqual(2)
+  })
+})

+ 110 - 62
frontend/components/Connection.js

@@ -19,30 +19,23 @@ const StyledConnection = styled.div`
   }
 `
 
-const CONNECTION_COMMAND = gql`
-  mutation CONNECTION_COMMAND(
-    $connectionId: ID!
-    $type: String!
-    $string: String!
-    $options: String
-  ) {
-    connectionCommand(
-      connectionId: $connectionId
-      type: $type
-      string: $string
-      options: $options
-    )
+const SEND_COMMAND = gql`
+  mutation SEND_COMMAND($connectionId: ID!, $command: ConnectionCommand!) {
+    sendCommand(connectionId: $connectionId, command: $command)
   }
 `
 
 const CONNECTION_QUERY = gql`
   query CONNECTION_QUERY($id: ID!) {
     connection(id: $id) {
-      workerInfo {
+      id
+      workerProcess {
         pid
         killed
-        exitCode
         signalCode
+        exitCode
+        spawnfile
+        spawnargs
       }
     }
   }
@@ -50,71 +43,126 @@ const CONNECTION_QUERY = gql`
 
 class Connection extends React.Component {
   state = {
-    command: ''
+    data: '',
+    type: 'ask',
+    typeCustom: ''
   }
 
   changeInput = event => {
-    this.setState({ [event.target.id]: event.target.value })
+    this.setState({ [event.target.name]: event.target.value })
   }
 
   render () {
-    const { id, device, interfaceName } = this.props.data
-    console.log(id, device, interfaceName)
+    const { id } = this.props.data
     return (
       <Mutation
-        mutation={CONNECTION_COMMAND}
+        mutation={SEND_COMMAND}
         variables={{
           connectionId: id,
-          type: 'ask',
-          string: this.state.command
+          command: {
+            type:
+              this.state.type === 'custom'
+                ? this.state.typeCustom
+                : this.state.type,
+            data: this.state.data
+          }
         }}
         refetchQueries={[{ query: INTERFACES_FULL }]}
         fetchPolicy='no-cache'
       >
-        {(connectionCommand, { data, error, loading }) => (
-          <StyledConnection>
-            <h1>Connection</h1>
-            <fieldset>
-              <label htmlFor='command'>Command</label>
+        {(sendCommand, { data, error, loading }) => {
+          return (
+            <StyledConnection>
+              <h1>Connection</h1>
+              <fieldset>
+                <label htmlFor='data'>Command</label>
+                <input
+                  type='text'
+                  value={this.state.data}
+                  onChange={this.changeInput}
+                  id='data'
+                  name='data'
+                  disabled={this.state.type === 'read'}
+                  placeholder='Command'
+                />
+              </fieldset>
+              <h2>Command type</h2>
+              <label htmlFor='type-read'>read</label>
+              <input
+                id='type-read'
+                name='type'
+                type='radio'
+                value='read'
+                checked={this.state.type === 'read'}
+                onChange={this.changeInput}
+              />
+              <label htmlFor='type-ask'>ask</label>
+              <input
+                id='type-ask'
+                name='type'
+                type='radio'
+                value='ask'
+                checked={this.state.type === 'ask'}
+                onChange={this.changeInput}
+              />
+              <label htmlFor='type-write'>write</label>
               <input
+                id='type-write'
+                name='type'
+                type='radio'
+                value='write'
+                checked={this.state.type === 'write'}
+                onChange={this.changeInput}
+              />
+              <label htmlFor='type-custom'>Custom...</label>
+              <input
+                id='type-custom'
+                name='type'
+                type='radio'
+                value='custom'
+                checked={this.state.type === 'custom'}
+                onChange={this.changeInput}
+              />
+              <input
+                id='typeCustom'
+                name='typeCustom'
                 type='text'
-                value={this.state.command}
+                name='typeCustom'
+                value={this.state.typeCustom}
+                disabled={this.state.type !== 'custom'}
                 onChange={this.changeInput}
-                id='command'
-                placeholder='Command'
               />
-            </fieldset>
-            <button
-              type='submit'
-              onClick={connectionCommand}
-              disabled={loading}
-            >
-              Send
-            </button>
-            <textarea
-              id='response'
-              value={data && data.connectionCommand}
-              readOnly
-            />
-            <textarea id='error' value={error} readOnly />
-            <Query query={CONNECTION_QUERY} variables={{ id }}>
-              {({ data, error, loading }) => {
-                if (loading) return null
-                if (error) return null
-                console.log(data)
-                const {
-                  connection: { workerInfo }
-                } = data
-                return (
-                  <>
-                    <p>pid: {workerInfo.pid}</p>
-                    <p>killed: {workerInfo.killed ? 'yes' : 'no'}</p>
-                  </>
-                )
-              }}
-            </Query>
-          </StyledConnection>
-        )}
+              <button type='submit' onClick={sendCommand} disabled={loading}>
+                Send
+              </button>
+              <textarea
+                id='response'
+                value={data && data.sendCommand}
+                readOnly
+              />
+              <textarea id='error' value={error} readOnly />
+              <Query query={CONNECTION_QUERY} variables={{ id }}>
+                {({ data, error, loading }) => {
+                  if (loading) return null
+                  if (error) return null
+                  const {
+                    connection: { workerProcess }
+                  } = data
+                  return (
+                    <>
+                      <p>pid: {workerProcess.pid}</p>
+                      <p>killed: {workerProcess.killed ? 'yes' : 'no'}</p>
+                      <p>exitCode: {workerProcess.exitCode}</p>
+                      <p>signalCode: {workerProcess.signalCode}</p>
+                      <p>spawnfile: {workerProcess.spawnfile}</p>
+                      <p>spawnargs: {workerProcess.spawnargs.join(', ')}</p>
+                    </>
+                  )
+                }}
+              </Query>
+            </StyledConnection>
+          )
+        }}
       </Mutation>
     )
   }

+ 77 - 29
frontend/components/FileUpload.js

@@ -10,7 +10,11 @@ const DO_LOCAL = gql`
 `
 
 const UPLOAD_FILE = gql`
-  mutation UPLOAD_FILE($files: [Upload!]!, $name: String!, $description: String!) {
+  mutation UPLOAD_FILE(
+    $files: [Upload!]!
+    $name: String!
+    $description: String!
+  ) {
     uploadFiles(files: $files, name: $name, description: $description) {
       id
       path
@@ -25,29 +29,51 @@ const UPLOAD_FILE = gql`
 
 const TestForm = props => {
   const [state, setState] = useState({ files: null, name: '', description: '' })
-  function updateState(event) {
+  function updateState (event) {
     const { name, type, files, value } = event.target
     setState({ ...state, [name]: type === 'file' ? files : value })
   }
   return (
     <Mutation mutation={UPLOAD_FILE} variables={state}>
       {(uploadFiles, { data, error, loading }) => (
-        <form onSubmit={async event => {
-          console.log(state)
-          event.preventDefault()
-          const res = await uploadFiles()
-          console.log(res)
-        }}>
-          <input type='file' multiple required name='files' onChange={updateState} />
-          <input type='text' name='name' value={state.name} onChange={updateState} />
-          <textarea name='description' value={state.description} onChange={updateState} />
+        <form
+          onSubmit={async event => {
+            console.log(state)
+            event.preventDefault()
+            const res = await uploadFiles()
+            console.log(res)
+          }}
+        >
+          <input
+            type='file'
+            multiple
+            required
+            name='files'
+            onChange={updateState}
+          />
+          <input
+            type='text'
+            name='name'
+            value={state.name}
+            onChange={updateState}
+          />
+          <textarea
+            name='description'
+            value={state.description}
+            onChange={updateState}
+          />
           <button>Send</button>
           <Query query={DO_LOCAL}>
             {({ data, client }) => (
               <>
-                <button type='button' onClick={
-                  event => { client.writeData({ data: { doLocal: !data.doLocal } }) }
-                }>Do Local</button>
+                <button
+                  type='button'
+                  onClick={event => {
+                    client.writeData({ data: { doLocal: !data.doLocal } })
+                  }}
+                >
+                  Do Local
+                </button>
                 <button disabled={data.doLocal}>Uh-Oh!</button>
               </>
             )}
@@ -67,19 +93,38 @@ const FileFormFields = props => {
       ...state,
       [name]: type === 'file' ? files[0] : value
     })
-    console.log(state)
     props.onChange(event, state)
   }
 
   return (
-    < fieldset >
+    <fieldset>
       <label htmlFor='file'>File</label>
-      <input type='file' multiple required name='file' id='file' onChange={updateState} />
+      <input
+        type='file'
+        multiple
+        required
+        name='file'
+        id='file'
+        onChange={updateState}
+      />
       <label htmlFor='name'>File name</label>
-      <input type='text' name='name' id='name' placeholder='File name' value={state.name} onChange={updateState} />
+      <input
+        type='text'
+        name='name'
+        id='name'
+        placeholder='File name'
+        value={state.name}
+        onChange={updateState}
+      />
       <label htmlFor='description'>File description</label>
-      <textarea name='description' id='description' placeholder='File description' value={state.description} onChange={updateState} />
-    </fieldset >
+      <textarea
+        name='description'
+        id='description'
+        placeholder='File description'
+        value={state.description}
+        onChange={updateState}
+      />
+    </fieldset>
   )
 }
 
@@ -94,7 +139,7 @@ const FileFields = {
   file: null
 }
 
-async function uploadFile(file) {
+async function uploadFile (file) {
   const body = new FormData()
   body.append('file', file)
 
@@ -142,19 +187,22 @@ class FileUpload extends React.Component {
     this.setState({ [event.target.name]: event.target.value })
   }
 
-  render() {
+  render () {
     return (
       <form>
         <fieldset>
-          <input type='file' name='file' id='file'
-            onChange={this.selectFile}
-          />
-          <textarea name="description" id="description" placeholder="Description"
+          <input type='file' name='file' id='file' onChange={this.selectFile} />
+          <textarea
+            name='description'
+            id='description'
+            placeholder='Description'
             value={this.state.description}
             onChange={this.handleChange}
-          ></textarea>
+          />
           <img src={this.state.path} alt={this.state.name} />
-          <button onClick={this.upload} disabled={this.state.uploading}>Save</button>
+          <button onClick={this.upload} disabled={this.state.uploading}>
+            Save
+          </button>
         </fieldset>
       </form>
     )
@@ -162,4 +210,4 @@ class FileUpload extends React.Component {
 }
 
 export default FileUpload
-export { FileFields, FileFormFields }
+export { FileFields, FileFormFields }

+ 12 - 7
frontend/components/Gallery.js

@@ -8,27 +8,32 @@ const GalleryStyle = styled.div`
     padding: 0 0.3em;
     background-color: ${props => props.theme.lighterblue};
     color: ${props => props.theme.darkblue};
-    cursor: pointer
+    cursor: pointer;
   }
 `
 
 class Gallery extends React.Component {
   state = {
-    galleryOpen: !!this.props.galleryOpen
+    galleryOpen: this.props.galleryOpen ? !!this.props.galleryOpen : true
   }
 
   openGallery = event => {
     this.setState({ galleryOpen: !this.state.galleryOpen })
   }
 
-  render() {
+  render () {
     const { items, title } = this.props
     return (
       <GalleryStyle>
-        <div onClick={this.openGallery} id="header">
-          {this.state.galleryOpen ? <span>&#8259;</span> : <span>&#8227;</span>} {items.length} {title} available (click to {this.state.galleryOpen ? 'hide' : 'see'})
+        <div onClick={this.openGallery} id='header'>
+          {this.state.galleryOpen ? <span>&#8259;</span> : <span>&#8227;</span>}{' '}
+          {items.length} {title} available (click to{' '}
+          {this.state.galleryOpen ? 'hide' : 'see'})
         </div>
-        <div style={{ display: this.state.galleryOpen ? 'block' : 'none' }} id="gallery">
+        <div
+          style={{ display: this.state.galleryOpen ? 'block' : 'none' }}
+          id='gallery'
+        >
           {items}
         </div>
       </GalleryStyle>
@@ -36,4 +41,4 @@ class Gallery extends React.Component {
   }
 }
 
-export default Gallery
+export default Gallery

+ 1 - 9
frontend/components/Interface.js

@@ -1,17 +1,9 @@
-import gql from 'graphql-tag'
 import { Query } from 'react-apollo'
 import Gallery from './Gallery'
 import Port from './Port'
 import Connection from './Connection'
 import InterfaceOption from './InterfaceOption'
-
-const INTERFACES = gql`
-  query INTERFACES {
-    interfaces {
-      name
-    }
-  }
-`
+import { INTERFACES } from './InterfaceList'
 
 const InterfaceFormFields = props => {
   const { state, onChange } = props

+ 0 - 1
frontend/components/InterfaceList.js

@@ -77,7 +77,6 @@ const InterfaceList = props => (
       if (loading) return <p>Loading interfaces...</p>
       if (error) return <p>Error loading interfaces: {error.message}</p>
       if (!data) return <p>No interfaces found.</p>
-      console.log(interfaces)
       const { interfaces } = data
       return (
         <div>

+ 14 - 8
frontend/components/Port.js

@@ -3,28 +3,34 @@ import gql from 'graphql-tag'
 import { INTERFACES_FULL } from './InterfaceList'
 
 const CONNECT_PORT = gql`
-  mutation CONNECT_PORT($interfaceName: String!, $device: String!) {
-    connect(interfaceName: $interfaceName, device: $device) {
+  mutation CONNECT_PORT($portId: ID!) {
+    connect(portId: $portId) {
       id
-      device
-      interfaceName
+      port {
+        interfaceName
+        host
+        device
+        name
+        description
+      }
     }
   }
 `
 
 class Port extends React.Component {
-  render() {
-    const { interfaceName, device, name, description } = this.props.data
+  render () {
+    const { id, device, name, description } = this.props.data
     return (
       <Mutation
         mutation={CONNECT_PORT}
-        variables={{ interfaceName, device }}
+        variables={{ portId: id }}
         refetchQueries={[{ query: INTERFACES_FULL }]}
       >
         {connect => (
           <div>
             <h1>{device}</h1>
-            <p>Name:</p><p>{name}</p>
+            <p>Name:</p>
+            <p>{name}</p>
             <p>{description}</p>
             <button onClick={connect}>Connect</button>
           </div>

+ 0 - 1
frontend/components/ProjectForm.js

@@ -154,7 +154,6 @@ const Project = props => {
         <form
           onSubmit={async event => {
             event.preventDefault()
-            console.log('form submitted.')
             const { data } = await createProject()
             state.id = data.createProject.id
           }}

+ 0 - 1
frontend/components/User.js

@@ -13,7 +13,6 @@ const CURRENT_USER = gql`
 `
 
 const User = props => {
-  console.log(props)
   return (
     <Query {...props} query={CURRENT_USER}>
       {payload => props.children(payload)}

+ 4 - 0
frontend/jest.setup.js

@@ -0,0 +1,4 @@
+import { configure } from 'enzyme'
+import Adapter from 'enzyme-adapter-react-16'
+
+configure({ adapter: new Adapter() })

+ 3 - 1
frontend/lib/localState.js

@@ -2,4 +2,6 @@ const resolvers = {}
 
 const typeDefs = {}
 
-export { resolvers, typeDefs }
+const initialState = { data: {} }
+
+export { resolvers, typeDefs, initialState }

+ 1 - 1
frontend/lib/parseSCPI.js

@@ -55,5 +55,5 @@ const DChar = new RegExp(`(${dchar})`)
 const block = `#${nzdig}(?:${dig})+(?:${dchar})*|#0(?:${dchar})*`
 const Block = new RegExp(`(${block})`)
 
-const header = ``
+const header = `v`
 const Header = new RegExp(`(${header})`)

+ 11 - 4
frontend/lib/withApollo.js

@@ -14,15 +14,20 @@ import { onError } from 'apollo-link-error'
 import { ApolloLink } from 'apollo-link'
 import { createUploadLink } from 'apollo-upload-client'
 import { endpoint, prodEndpoint } from '../config'
-import { resolvers, typeDefs } from './localState'
+import { resolvers, typeDefs, initialState } from './localState'
 
-function createClient ({ ctx, headers, initialState }) {
+const cache = new InMemoryCache()
+
+function createClient ({ ctx, headers }) {
   return new ApolloClient({
     link: ApolloLink.from([
       onError(({ graphQLErrors, networkError }) => {
         if (graphQLErrors) {
           graphQLErrors.map(({ message, locations, path }) =>
-            console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`))
+            console.log(
+              `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
+            )
+          )
         }
         if (networkError) console.log(`[Network error]: ${networkError}`)
       }),
@@ -33,10 +38,12 @@ function createClient ({ ctx, headers, initialState }) {
       })
     ]),
     fetchOptions: { mode: 'no-cors' },
-    cache: new InMemoryCache(),
+    cache,
     resolvers,
     typeDefs
   })
 }
 
+cache.writeData(initialState)
+
 export default withApollo(createClient)

Fichier diff supprimé car celui-ci est trop grand
+ 794 - 4
frontend/package-lock.json


+ 21 - 3
frontend/package.json

@@ -6,7 +6,7 @@
   "scripts": {
     "start": "next",
     "build": "next build",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "NODE_ENV=test jest --watch"
   },
   "keywords": [],
   "author": "",
@@ -35,6 +35,24 @@
     "styled-components": "^4.2.0"
   },
   "devDependencies": {
-    "babel-plugin-styled-components": "^1.10.0"
+    "babel-jest": "^24.8.0",
+    "babel-plugin-styled-components": "^1.10.0",
+    "jest": "^24.8.0",
+    "jest-transform-graphql": "^2.1.0",
+    "enzyme": "^3.9.0",
+    "enzyme-adapter-react-16": "^1.12.1"
+  },
+  "jest": {
+    "setupFilesAfterEnv": [
+      "<rootDir>/jest.setup.js"
+    ],
+    "testPathIgnorePatterns": [
+      "<rootDir>/.next/",
+      "<rootDir>/node_modules/"
+    ],
+    "transform": {
+      "\\.(gql|graphql)$": "jest-transform-graphql",
+      ".*": "babel-jest"
+    }
   }
-}
+}

+ 0 - 1
frontend/pages/instruments.js

@@ -6,7 +6,6 @@ const InstrumentsPage = props =>
   props.query && props.query.id ? (
     <Query query={INSTRUMENT_QUERY} variables={{ id: props.query.id }}>
       {({ data, loading, error }) => {
-        console.log(data, loading, error)
         if (loading) return <p>Loading instrument...</p>
         if (error) return <p>Error loading instrument: {error.message}</p>
         const { instrument } = data

+ 1 - 4
frontend/pages/interfaces.js

@@ -1,8 +1,5 @@
-import Connection from '../components/Connection'
 import InterfaceList from '../components/InterfaceList'
 
-const InterfacePage = props => (
-  <InterfaceList />
-)
+const InterfacePage = props => <InterfaceList />
 
 export default InterfacePage

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff