diff --git a/Development/src/components/PaginationButtons.js b/Development/src/components/PaginationButtons.js
index 67ab71f..caaf3b2 100644
--- a/Development/src/components/PaginationButtons.js
+++ b/Development/src/components/PaginationButtons.js
@@ -36,7 +36,7 @@ export const PaginationButton = ({
     };
 
     return (
-        <Button onClick={() => nextPage(rel)} disabled={!enabled}>
+        <Button onClick={() => nextPage(rel)} disabled={!enabled} name={rel}>
             {getIcon(rel)}
             {label}
         </Button>
diff --git a/Development/src/pages/receivers/ConnectButtons.js b/Development/src/pages/receivers/ConnectButtons.js
index 529ae41..a084ad2 100644
--- a/Development/src/pages/receivers/ConnectButtons.js
+++ b/Development/src/pages/receivers/ConnectButtons.js
@@ -65,6 +65,7 @@ const ConnectButtons = ({ senderData, receiverData }) => {
                 onClick={event => handleConnect('active', event)}
                 color="primary"
                 startIcon={<ActivateImmediateIcon />}
+                name="activate"
             >
                 Activate
             </Button>
diff --git a/Development/src/pages/receivers/ConnectionManagementTab.js b/Development/src/pages/receivers/ConnectionManagementTab.js
index c4c18b6..71e55ba 100644
--- a/Development/src/pages/receivers/ConnectionManagementTab.js
+++ b/Development/src/pages/receivers/ConnectionManagementTab.js
@@ -201,6 +201,7 @@ const ConnectionManagementTab = ({ receiverData, basePath }) => {
                                             style={{
                                                 textDecoration: 'none',
                                             }}
+                                            name="label"
                                         >
                                             <LinkChipField record={item} />
                                         </Link>
diff --git a/Development/src/pages/receivers/ReceiversList.js b/Development/src/pages/receivers/ReceiversList.js
index d842eb2..e78175d 100644
--- a/Development/src/pages/receivers/ReceiversList.js
+++ b/Development/src/pages/receivers/ReceiversList.js
@@ -96,6 +96,7 @@ const ReceiversList = props => {
                                             basePath="/receivers"
                                             record={item}
                                             label={item.label}
+                                            name="label"
                                         />
                                     </TableCell>
                                     <TableCell>
@@ -117,6 +118,7 @@ const ReceiversList = props => {
                                             <ActiveField
                                                 record={item}
                                                 resource="receivers"
+                                                name="active"
                                             />
                                         </TableCell>
                                     )}
diff --git a/Development/src/pages/receivers/ReceiversShow.js b/Development/src/pages/receivers/ReceiversShow.js
index d00d973..bce89b3 100644
--- a/Development/src/pages/receivers/ReceiversShow.js
+++ b/Development/src/pages/receivers/ReceiversShow.js
@@ -104,6 +104,7 @@ const ReceiversShowView = props => {
                                 disabled={
                                     !get(record, `$${key}`) || !useConnectionAPI
                                 }
+                                name={key}
                             />
                         ))}
                         <Tab
@@ -114,6 +115,7 @@ const ReceiversShowView = props => {
                             disabled={
                                 !get(record, '$staged') || !useConnectionAPI
                             }
+                            name="connect"
                         />
                     </Tabs>
                 </Paper>
@@ -140,10 +142,10 @@ const ShowSummaryTab = ({ record, ...props }) => {
     return (
         <ShowView {...props} title={<ResourceTitle />} actions={<Fragment />}>
             <SimpleShowLayout>
-                <TextField label="ID" source="id" />
+                <TextField label="ID" source="id" name="id" />
                 <TAIField source="version" />
-                <TextField source="label" />
-                <TextField source="description" />
+                <TextField source="label" name="label" />
+                <TextField source="description" name="description" />
                 <ObjectField source="tags" />
                 <SanitizedDivider />
                 <ParameterField source="transport" register={TRANSPORTS} />
@@ -181,7 +183,11 @@ const ShowSummaryTab = ({ record, ...props }) => {
                 )}
                 <ParameterField source="format" register={FORMATS} />
                 {queryVersion() >= 'v1.2' && (
-                    <BooleanField label="Active" source="subscription.active" />
+                    <BooleanField
+                        label="Active"
+                        source="subscription.active"
+                        name="active"
+                    />
                 )}
                 {queryVersion() >= 'v1.2' &&
                     get(record, 'subscription.sender_id') && (
@@ -220,6 +226,7 @@ const ShowActiveTab = ({ record, ...props }) => {
                         source="$active.sender_id"
                         reference="senders"
                         link="show"
+                        name="sender"
                     >
                         <LinkChipField />
                     </ReferenceField>
@@ -227,6 +234,7 @@ const ShowActiveTab = ({ record, ...props }) => {
                 <BooleanField
                     label="Master Enable"
                     source="$active.master_enable"
+                    name="master_enable"
                 />
                 <TextField label="Mode" source="$active.activation.mode" />
                 <TAIField
diff --git a/Development/src/pages/senders/SendersList.js b/Development/src/pages/senders/SendersList.js
index 1156da4..19b3062 100644
--- a/Development/src/pages/senders/SendersList.js
+++ b/Development/src/pages/senders/SendersList.js
@@ -90,6 +90,7 @@ const SendersList = props => {
                                             basePath="/senders"
                                             record={item}
                                             label={item.label}
+                                            name="label"
                                         />
                                     </TableCell>
                                     <TableCell>
diff --git a/Development/src/pages/senders/SendersShow.js b/Development/src/pages/senders/SendersShow.js
index dc0d5eb..d13f434 100644
--- a/Development/src/pages/senders/SendersShow.js
+++ b/Development/src/pages/senders/SendersShow.js
@@ -120,10 +120,10 @@ const ShowSummaryTab = ({ record, ...props }) => {
     return (
         <ShowView {...props} title={<ResourceTitle />} actions={<Fragment />}>
             <SimpleShowLayout>
-                <TextField label="ID" source="id" />
+                <TextField label="ID" source="id" name="id" />
                 <TAIField source="version" />
-                <TextField source="label" />
-                <TextField source="description" />
+                <TextField source="label" name="label" />
+                <TextField source="description" name="description" />
                 <ObjectField source="tags" />
                 <SanitizedDivider />
                 <ParameterField source="transport" register={TRANSPORTS} />
diff --git a/Development/src/pages/settings.js b/Development/src/pages/settings.js
index b8b48cc..b497f35 100644
--- a/Development/src/pages/settings.js
+++ b/Development/src/pages/settings.js
@@ -95,6 +95,7 @@ const Settings = () => {
                                     onFocus={selectOnFocus}
                                     disabled={disabledSetting(QUERY_API)}
                                     helperText="Used to show the registered Nodes and their sub-resources"
+                                    name="queryapi"
                                 />
                             </StyledListItem>
                         )}
diff --git a/TestingFacade/.flake8 b/TestingFacade/.flake8
new file mode 100644
index 0000000..6deafc2
--- /dev/null
+++ b/TestingFacade/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120
diff --git a/TestingFacade/Config.py b/TestingFacade/Config.py
new file mode 100644
index 0000000..b26f71e
--- /dev/null
+++ b/TestingFacade/Config.py
@@ -0,0 +1,12 @@
+# Port for Testing Facade to run on
+TESTING_FACADE_PORT = 5001
+# URL of nmos-js instance for testing
+NCUT_URL = "http://localhost:3000/#/"
+# URL of NMOS Testing Tool's mock registry
+MOCK_REGISTRY_URL = "http://127.0.0.1:5102/"
+# Browser to use for testing. Matching webdriver must be installed
+BROWSER = 'Chrome'
+# Use browser in headless mode. Set to False to have each test run in a visible window
+HEADLESS = True
+# Time in seconds to wait for elements to load
+WAIT_TIME = 5
diff --git a/TestingFacade/DataStore.py b/TestingFacade/DataStore.py
new file mode 100644
index 0000000..925c0e4
--- /dev/null
+++ b/TestingFacade/DataStore.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2021 Advanced Media Workflow Association
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class DataStore:
+    """
+    Store json with test question details for use with NMOS Controller
+    test suite and Testing Facade
+    """
+
+    def __init__(self):
+        self.test_type = None
+        self.question_id = None
+        self.name = None
+        self.description = None
+        self.question = None
+        self.answers = None
+        self.timeout = None
+        self.answer_uri = None
+        self.answer_response = None
+        self.metadata = None
+
+    def clear(self):
+        self.test_type = None
+        self.question_id = None
+        self.name = None
+        self.description = None
+        self.question = None
+        self.answers = None
+        self.timeout = None
+        self.answer_uri = None
+        self.answer_response = None
+        self.metadata = None
+
+    def setJson(self, json):
+        self.test_type = json["test_type"]
+        self.question_id = json["question_id"]
+        self.name = json["name"]
+        self.description = json["description"]
+        self.question = json["question"]
+        self.answers = json["answers"]
+        self.timeout = json['timeout']
+        self.answer_uri = json["answer_uri"]
+        self.metadata = json["metadata"]
+
+    def getAnswerJson(self):
+        json_data = {
+            "question_id": self.question_id,
+            "answer_response": self.answer_response
+        }
+        return json_data
+
+    def setAnswer(self, answer):
+        self.answer_response = answer
+
+    def getQuestionID(self):
+        return self.question_id
+
+    def getAnswers(self):
+        return self.answers
+
+    def getUrl(self):
+        return self.answer_uri
+
+    def getMetadata(self):
+        return self.metadata
+
+
+dataStore = DataStore()
diff --git a/TestingFacade/GenericAutoTest.py b/TestingFacade/GenericAutoTest.py
new file mode 100644
index 0000000..883e322
--- /dev/null
+++ b/TestingFacade/GenericAutoTest.py
@@ -0,0 +1,128 @@
+import time
+from selenium import webdriver
+from selenium.webdriver.common.keys import Keys
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+import Config as CONFIG
+
+class GenericAutoTest:
+    """
+    Base test class for automated version of NMOS Controller test suite without Testing Façade
+    """
+    def __init__(self):
+        self.NCuT_url = CONFIG.NCUT_URL
+        self.mock_registry_url = CONFIG.MOCK_REGISTRY_URL
+        self.multipart_question_storage = {}
+
+    def set_up_test(self):
+        # Set up webdriver
+        browser = getattr(webdriver, CONFIG.BROWSER)
+        get_options = getattr(webdriver, CONFIG.BROWSER + 'Options', False)
+        if get_options:
+            options = get_options()   
+            options.headless = CONFIG.HEADLESS
+            self.driver = browser(options=options)
+        else:
+            self.driver = browser()
+        self.driver.implicitly_wait(CONFIG.WAIT_TIME)
+        # Launch browser, navigate to nmos-js and update query api url to mock registry
+        self.driver.get(self.NCuT_url + "Settings")
+        query_api = self.driver.find_element_by_name("queryapi")
+        query_api.clear()
+        if query_api.get_attribute('value') != '':
+            time.sleep(1)
+            query_api.send_keys(Keys.CONTROL + "a")
+            query_api.send_keys(Keys.DELETE)
+        query_api.send_keys(self.mock_registry_url + "x-nmos/query/v1.3")
+        # Open menu to show link names if not already open
+        open_menu = self.driver.find_elements_by_xpath('//*[@title="Open menu"]')
+        if open_menu:
+            open_menu[0].click()
+
+    def tear_down_test(self):
+        self.driver.close()
+
+    def refresh_page(self):
+        """
+        Click refresh button and sleep to allow loading time
+        """
+        self.driver.find_element_by_css_selector("[aria-label='Refresh']").click()
+        time.sleep(1)
+
+    def navigate_to_page(self, page):
+        """
+        Navigate to page by link text, refresh page and sleep to allow loading time
+        """
+        self.driver.find_element_by_link_text(page).click()
+        self.refresh_page()
+
+    def find_resource_labels(self):
+        """
+        Find all resources on a page by label
+        Returns list of labels
+        """
+        resources = self.driver.find_elements_by_name("label")
+        return [entry.text for entry in resources]
+
+    def next_page(self):
+        """
+        Navigate to next page via next button and sleep to allow loading time
+        """
+        self.driver.find_element_by_name('next').click()
+        time.sleep(1)
+
+    def check_connectable(self):
+        """
+        Check if connect tab is active
+        returns True if available, False if disabled
+        """
+        connect_button = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located((By.NAME,
+                                                                                                "connect")))
+        disabled = connect_button.get_attribute("aria-disabled")
+        return True if disabled == 'false' else False
+
+    def make_connection(self, sender):
+        """
+        Navigate to connect tab, activate connection to given sender
+        """
+        connect = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "connect")))
+        connect.click()
+
+        # Find the row containing the correct sender and activate connection
+        senders = self.find_resource_labels()
+        row = [i for i, s in enumerate(senders) if s == sender][0]
+        activate_button = self.driver.find_elements_by_name("activate")[row]
+        activate_button.click()
+        time.sleep(2)
+
+    def remove_connection(self, receiver):
+        """
+        Deactivate a connection on a given receiver
+        """
+        receivers = self.find_resource_labels()
+        row = [i for i, r in enumerate(receivers) if r == receiver][0]
+        deactivate_button = self.driver.find_elements_by_name("active")[row]
+        if deactivate_button.get_attribute('value') == "true":
+            deactivate_button.click()
+        time.sleep(2)
+
+    def get_active_receiver(self):
+        """
+        Identify an active receiver
+        Returns string of receiver label or None
+        """
+        active_buttons = self.driver.find_elements_by_name('active')
+        active_row = [i for i, b in enumerate(active_buttons) if b.get_attribute('value') == "true"]
+        return None if not active_row else self.driver.find_elements_by_name('label')[active_row[0]].text
+    
+    def get_connected_sender(self):
+        """
+        Identify the sender a receiver is connected to
+        Returns string of sender label
+        """
+        active = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "active")))
+        active.click()
+
+        return self.driver.find_element_by_name('sender').text
\ No newline at end of file
diff --git a/TestingFacade/IS0404AutoTest.py b/TestingFacade/IS0404AutoTest.py
new file mode 100644
index 0000000..bc0fa43
--- /dev/null
+++ b/TestingFacade/IS0404AutoTest.py
@@ -0,0 +1,153 @@
+import time
+from GenericAutoTest import GenericAutoTest
+
+
+class IS0404AutoTest(GenericAutoTest):
+    """
+    Automated version of NMOS Controller test suite IS0404
+    """
+
+    def test_01(self, answers, metadata):
+        """
+        Ensure NCuT uses DNS-SD to find registry
+        """
+        return "NMOS-js does not use DNS-SD to find registry"
+
+    def test_02(self, answers, metadata):
+        """
+        Ensure NCuT can access the IS-04 Query API
+        """
+        # Use the NCuT to browse the Senders and Receivers on the
+        # discovered Registry via the selected IS-04 Query API.
+        # Once you have finished browsing click the 'Next' button.
+        # Successful browsing of the Registry will be automatically
+        # logged by the test framework.
+
+        self.navigate_to_page('Senders')
+        self.navigate_to_page('Receivers')
+
+    def test_03(self, answers, metadata):
+        """
+        Query API should be able to discover all the senders that are
+        registered in the Registry
+        """
+        # The NCuT should be able to discover all the Senders that are
+        # registered in the Registry.
+        # Refresh the NCuT's view of the Registry and carefully select
+        # the Senders that are available from the following list.
+        # For this test the registry paging limit has been set to 2.
+        # If your NCuT implements pagination, you must ensure you view
+        # every available page to complete this test.
+
+        self.navigate_to_page('Senders')
+        
+        # Loop through pages gathering all senders        
+        actual_answers = []
+
+        for i in range(len(answers)):
+            senders = self.find_resource_labels()
+            if not senders:
+                break
+            actual_answers += [answer['answer_id'] for answer in answers if answer['resource']['label']
+                               in senders]
+            self.next_page()
+
+        return actual_answers
+
+    def test_04(self, answers, metadata):
+        """
+        Query API should be able to discover all the receivers that are
+        registered in the Registry
+        """
+        # The NCuT should be able to discover all the Receivers that are
+        # registered in the Registry.
+        # Refresh the NCuT's view of the Registry and carefully select
+        # the Receivers that are available from the following list.
+        # For this test the registry paging limit has been set to 2.
+        # If your NCuT implements pagination, you must ensure you view
+        # every available page to complete this test.
+
+        self.navigate_to_page('Receivers')
+        
+        # Loop through pages gathering all receivers        
+        actual_answers = []
+
+        for i in range(len(answers)):
+            receivers = self.find_resource_labels()
+            if not receivers:
+                break
+            actual_answers += [answer['answer_id'] for answer in answers if answer['resource']['label']
+                               in receivers]
+            self.next_page()
+
+        return actual_answers
+
+    def test_05(self, answers, metadata):
+        """
+        Reference Sender is put offline then back online
+        First question
+        """
+        # The NCuT should be able to discover and dynamically update all
+        # the Senders that are registered in the Registry.
+        # Use the NCuT to browse and take note of the Senders that are
+        # available.
+        # After the 'Next' button has been clicked one of those senders
+        # will be put 'offline'.
+
+        # Save current list of senders
+        self.navigate_to_page('Senders')
+        self.multipart_question_storage['test_05'] = self.find_resource_labels()
+
+    def test_05_1(self, answers, metadata):
+        """
+        Reference Sender is put offline then back online
+        Second question
+        """
+        # Please refresh your NCuT and select the sender which has been put
+        # 'offline'
+
+        # Get current list of senders and compare against previously saved list
+        self.navigate_to_page('Senders')
+        sender_list = self.find_resource_labels()
+        offline_sender = list(set(self.multipart_question_storage['test_05']) - set(sender_list))
+        # Save offline sender
+        self.multipart_question_storage['test_05_1'] = offline_sender[0]
+
+        actual_answer = [answer['answer_id'] for answer in answers
+                         if answer['resource']['label'] == offline_sender[0]][0]
+
+        return actual_answer
+
+    def test_05_2(self, answers, metadata):
+        """
+        Reference Sender is put offline then back online
+        Third question
+        """
+        # The sender which was put 'offline' will come back online at a
+        # random moment within the next x seconds.
+        # As soon as the NCuT detects the sender has come back online
+        # please press the 'Next' button.
+        # The button must be pressed within x seconds of the Sender being
+        # put back 'online'.
+        # This includes any latency between the Sender being put 'online'
+        # and the NCuT updating.
+
+        self.navigate_to_page('Senders')
+        sender_list = set()
+
+        # Find all senders, keep checking until same as number of senders at start of test
+        while len(sender_list) < len(self.multipart_question_storage['test_05']):
+            time.sleep(4)
+            self.refresh_page()
+            senders = self.find_resource_labels()
+            sender_list.update(senders)
+            last_sender = senders[-1]
+
+        # Check same sender came back
+        if last_sender == self.multipart_question_storage['test_05_1']:
+            return "Next"
+        else:
+            return "Unrecognised Sender"
+
+
+IS0404tests = IS0404AutoTest()
diff --git a/TestingFacade/IS0503AutoTest.py b/TestingFacade/IS0503AutoTest.py
new file mode 100644
index 0000000..fba993f
--- /dev/null
+++ b/TestingFacade/IS0503AutoTest.py
@@ -0,0 +1,132 @@
+import time
+from GenericAutoTest import GenericAutoTest
+
+
+class IS0503AutoTest(GenericAutoTest):
+    """
+    Automated version of NMOS Controller test suite IS0503
+    """
+    def test_01(self, answers, metadata):
+        """
+        Identify which Receiver devices are controllable via IS-05
+        """
+        # A subset of the Receivers registered with the Registry are controllable via IS-05,
+        # for instance, allowing Senders to be connected.
+        # Please refresh your NCuT and select the Receivers
+        # that have a Connection API from the list below.
+        # Be aware that if your NCuT only displays Receivers which have a Connection API,
+        # some of the Receivers in the following list may not be visible.
+
+        self.navigate_to_page('Receivers')
+        receivers = self.find_resource_labels()
+        
+        # Loop through receivers and check if connection tab is disabled
+        connectable_receivers = []
+        
+        for receiver in receivers:
+            self.navigate_to_page(receiver)
+            connectable = self.check_connectable()
+            if connectable:
+                connectable_receivers.append(receiver)
+            self.navigate_to_page('Receivers')
+
+        # Get answer ids for connectable receivers to send to test suite
+        actual_answers = [answer['answer_id'] for answer in answers if answer['resource']['label']
+                          in connectable_receivers]
+
+        return actual_answers
+
+    def test_02(self, answers, metadata):
+        """
+        Instruct Receiver to subscribe to a Sender’s Flow via IS-05
+        """
+        # All flows that are available in a Sender should be able to be
+        # connected to a Receiver.
+        # Use the NCuT to perform an 'immediate' activation between
+        # sender: x and receiver: y
+        # Click the 'Next' button once the connection is active.
+
+        # Get sender and receiver details from metadata sent with question
+        sender = metadata['sender']
+        receiver = metadata['receiver']
+
+        self.navigate_to_page('Receivers')
+        self.navigate_to_page(receiver['label'])
+        self.make_connection(sender['label'])
+
+    def test_03(self, answers, metadata):
+        """
+        Disconnecting a Receiver from a connected Flow via IS-05
+        """
+        # IS-05 provides a mechanism for removing an active connection
+        # through its API.
+        # Use the NCuT to remove the connection between sender: x and
+        # receiver: y
+        # Click the 'Next' button once the connection has been removed.'
+
+        receiver = metadata['receiver']
+        self.navigate_to_page('Receivers')
+        self.remove_connection(receiver['label'])
+
+    def test_04(self, answers, metadata):
+        """
+        Indicating the state of connections via updates received from the
+        IS-04 Query API
+        First question
+        """
+        # The NCuT should be able to monitor and update the connection status
+        # of all registered Devices.
+        # Use the NCuT to identify the receiver that has just been activated.
+
+        self.navigate_to_page('Receivers')
+        receiver = self.get_active_receiver()
+
+        actual_answer = [answer['answer_id'] for answer in answers if answer['resource']['label'] == receiver][0]
+
+        return actual_answer
+
+    def test_04_1(self, answers, metadata):
+        """
+        Indicating the state of connections via updates received from the
+        IS-04 Query API
+        Second question
+        """
+        # Use the NCuT to identify the sender currently connected to receiver x
+        receiver = metadata['receiver']
+        self.navigate_to_page('Receivers')
+        self.navigate_to_page(receiver['label'])
+        sender = self.get_connected_sender()
+
+        actual_answer = [answer['answer_id'] for answer in answers if answer['resource']['label'] == sender][0]
+
+        return actual_answer
+
+    def test_04_2(self, answers, metadata):
+        """
+        Indicating the state of connections via updates received from the
+        IS-04 Query API
+        Third Question
+        """
+        # The connection on the following receiver will be disconnected
+        # at a random moment within the next x seconds.
+        # receiver x
+        # As soon as the NCuT detects the connection is inactive please
+        # press the 'Next' button.
+        # The button must be pressed within x seconds of the connection
+        # being removed.
+        # This includes any latency between the connection being removed
+        # and the NCuT updating.
+
+        receiver = metadata['receiver']
+        self.navigate_to_page('Receivers')
+
+        # Periodically refresh until no receiver is active
+        for i in range(1, 20):
+            time.sleep(4)
+            self.refresh_page()
+            receiver = self.get_active_receiver()
+            if not receiver:
+                break
+
+
+IS0503tests = IS0503AutoTest()
diff --git a/TestingFacade/README.md b/TestingFacade/README.md
new file mode 100644
index 0000000..4f6b3c7
--- /dev/null
+++ b/TestingFacade/README.md
@@ -0,0 +1,30 @@
+# NMOS Controller testing - Fully Automated testing of nmos-js
+
+## Installation and usage
+
+1. Install flask and selenium
+`pip install -r requirements.txt`
+
+2. Install the webdriver for the browser you wish to use. [See selenium docs for more info.](https://www.selenium.dev/documentation/en/webdriver/driver_requirements/#quick-reference) 
+
+3. Update the configuration file `Config.py` if necessary. 
+    - `NCUT_URL` the url of your instance of nmos-js 
+    - `MOCK_REGISTRY_URL` the url of the mock registry set up by the NMOS Controller test suite, included in the `pre_tests_message`
+    - `BROWSER` the name of the browser for which you installed the driver in step 2  
+
+4. Run the NMOS Testing tool (https://github.com/AMWA-TV/nmos-testing) and choose a Controller test suite
+
+5. Run TestingFacade.py with your chosen test suite specified using the `--suite` command line argument. Eg. `python3 TestingFacade.py --suite 'IS0404'`.  
+Currently supported test suites are:  
+    - IS0404  
+    - IS0503  
+
+6. On your NMOS Testing instance enter the IP address and Port where the Automated Testing Facade is running
+
+7. Choose tests and click Run. The Testing Facade will launch a new headless browser at the beginning of each test and close it at the end. Note: Set the value of `HEADLESS` in `Config.py` to `False` to have the tests run in visible browser windows
+
+8. Test suite `POST`s the Question JSON to the TestingFacade API endpoint `/x-nmos/testquestion/{version}`. TestingFacade will retrieve the data and run the relevant set of selenium instructions defined in the test suite file to complete the test in your chosen browser then `POST`s the Answer JSON back to the test suite via the endpoint given in the `answer_uri` of the Question
+
+9. After the last test, test suite will `POST` a clear request to empty the data store
+
+10. Results are displayed on NMOS Testing tool
diff --git a/TestingFacade/TestingFacade.py b/TestingFacade/TestingFacade.py
new file mode 100644
index 0000000..40e07db
--- /dev/null
+++ b/TestingFacade/TestingFacade.py
@@ -0,0 +1,111 @@
+# Copyright (C) 2021 Advanced Media Workflow Association
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import requests
+import sys
+from threading import Thread
+from flask import Flask, request
+from selenium.common.exceptions import NoSuchElementException
+from DataStore import dataStore
+from IS0404AutoTest import IS0404tests
+from IS0503AutoTest import IS0503tests
+import Config as CONFIG
+
+TEST_SETS = {'IS0404': IS0404tests,
+             'IS0503': IS0503tests}
+
+app = Flask(__name__)
+
+
+@app.route('/x-nmos/testquestion/<version>', methods=['POST'], strict_slashes=False)
+def controller_tests_post(version):
+    # Should be json from Test Suite with questions
+    expected_entries = ['test_type', 'name', 'description', 'question', 'answers', 'answer_uri']
+
+    if request.json.get('clear'):
+        # End of current tests, clear data store
+        dataStore.clear()
+    else:
+        # Should be a new question
+        for entry in expected_entries:
+            if entry not in request.json:
+                return 'Invalid JSON received', 400
+        # All required entries are present so update data
+        dataStore.setJson(request.json)
+        # Run test in new thread
+        executionThread = Thread(target=execute_test)
+        executionThread.start()
+    return '', 202
+
+
+def execute_test():
+    """
+    After test data has been sent to x-nmos/testing-facade figure out which
+    test was sent. Call relevant test method and retrieve answers.
+    Update json and send back to test suite
+    """
+    # Get question details from data store
+    question_id = dataStore.getQuestionID()
+    answers = dataStore.getAnswers()
+    metadata = dataStore.getMetadata()
+
+    # Load specified test suite
+    tests = TEST_SUITE
+
+    if question_id.startswith("test_"):
+        # Get method associated with question id, set up test browser,
+        # run method then tear down and save any answers returned to data store
+        method = getattr(tests, question_id)
+        if callable(method):
+            print(" * Running " + question_id)
+            try:
+                tests.set_up_test()
+                test_result = method(answers, metadata)
+            except NoSuchElementException:
+                test_result = None
+            tests.tear_down_test()
+            dataStore.setAnswer(test_result)
+
+    elif question_id == 'pre_tests_message':
+        # Beginning of test set, return to confirm start
+        dataStore.setAnswer(None)
+
+    elif question_id == 'post_tests_message':
+        # End of test set, return to confirm end
+        dataStore.setAnswer(None)
+        print(' *** Tests Complete ***')
+
+    else:
+        # Not a recognised part of test suite
+        dataStore.setAnswer(None)
+
+    # POST answer json back to test suite
+    requests.post(dataStore.getUrl(), json=dataStore.getAnswerJson())
+    return
+
+
+if __name__ == "__main__":
+    global TEST_SUITE
+
+    if '--suite' not in sys.argv:
+        sys.exit('You must specify a test suite with --suite')
+
+    for i, arg in enumerate(sys.argv):
+        if arg == '--suite':
+            try:
+                TEST_SUITE = TEST_SETS[sys.argv[i+1]]
+            except Exception as e:
+                sys.exit('Invalid test suite selection ' + str(e))
+
+    app.run(host='0.0.0.0', port=CONFIG.TESTING_FACADE_PORT)
diff --git a/TestingFacade/requirements.txt b/TestingFacade/requirements.txt
new file mode 100644
index 0000000..14f4c60
--- /dev/null
+++ b/TestingFacade/requirements.txt
@@ -0,0 +1,3 @@
+flask>=1.0.0
+selenium
+requests
\ No newline at end of file