├── .coveralls.yml ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── azure-static-web-apps-mango-grass-0e1bab20f.yml │ ├── codeql.yml │ ├── coverage-base-on-master.yml.upgrade-to-remove-express │ ├── coverage.yml.upgrade-to-remove-express │ ├── cypress-tests.yml.upgrade-to-remove-express │ └── lint.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE.md ├── README.md ├── api ├── .funcignore ├── .gitignore ├── context │ ├── function.json │ └── index.js ├── dist-proxy │ ├── function.json │ └── index.js ├── host.json ├── package-lock.json ├── package.json ├── proxies.json └── status │ ├── function.json │ └── index.js ├── architecture.md ├── babel.config.json ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── .examples │ │ ├── actions.spec.js │ │ ├── aliasing.spec.js │ │ ├── assertions.spec.js │ │ ├── connectors.spec.js │ │ ├── cookies.spec.js │ │ ├── cypress_api.spec.js │ │ ├── files.spec.js │ │ ├── local_storage.spec.js │ │ ├── location.spec.js │ │ ├── misc.spec.js │ │ ├── navigation.spec.js │ │ ├── network_requests.spec.js │ │ ├── querying.spec.js │ │ ├── spies_stubs_clocks.spec.js │ │ ├── traversal.spec.js │ │ ├── utilities.spec.js │ │ ├── viewport.spec.js │ │ ├── waiting.spec.js │ │ └── window.spec.js │ ├── authentication.spec.js │ ├── plan.spec.js │ ├── realtime.spec.js │ └── window.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── dist ├── MatrXCloseWhite.png ├── index.html ├── matrx-favicon.png ├── matrxlogowhite.png └── routes.json ├── lerna.json ├── matrx.code-workspace ├── package-lock.json ├── package.json ├── packages ├── dragster │ ├── LICENSE.md │ ├── README.md │ ├── __tests__ │ │ └── dragster.test.js │ ├── dragster.js │ ├── package-lock.json │ └── package.json ├── svelte-realtime-adapter-cosmos-db-temporal │ ├── LICENSE.md │ ├── README.md │ ├── cypress.json │ ├── package-lock.json │ ├── package.json │ ├── stored-procedures │ │ └── operations.js │ ├── svelte-realtime-adapter-cosmos-db-temporal.js │ └── test │ │ └── single-entity-saves.spec.js ├── svelte-realtime-server │ ├── LICENSE.md │ ├── README.md │ ├── __tests__ │ │ └── realtime.test.js │ ├── package-lock.json │ ├── package.json │ └── svelte-realtime-server.cjs ├── svelte-realtime-store │ ├── LICENSE.md │ ├── README.md │ ├── __tests__ │ │ └── realtime.test.js │ ├── package-lock.json │ ├── package.json │ └── svelte-realtime-store.js └── svelte-viewstate-store │ ├── LICENSE.md │ ├── README.md │ ├── __tests__ │ └── svelte-viewstate-store.test.js │ ├── package-lock.json │ ├── package.json │ └── svelte-viewstate-store.js ├── src ├── App.svelte ├── main.js ├── matrx.scss ├── router.js ├── routes │ ├── Home.svelte │ ├── Login.svelte │ ├── Morgan.svelte │ ├── NotFound.svelte │ ├── Plan │ │ ├── DoingKanban.svelte │ │ ├── FormulationGrid.svelte │ │ ├── KanbanCell.svelte │ │ ├── Plan.svelte │ │ ├── PracticeEditor.svelte │ │ ├── index.js │ │ └── plan-helpers.js │ ├── Poc.svelte │ ├── Progress.svelte │ ├── Regex.svelte │ └── TestJig.svelte ├── server.js └── stores.js ├── svelte.config.js ├── to-do.md ├── webpack.config.coverage-client.js └── webpack.config.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: xXk1ykoyFtRbg7uArLfnkPq7xxguhN3VI -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | dist 3 | 4 | # node_modules 5 | node_modules 6 | 7 | # tests 8 | __tests__/ 9 | 10 | # others 11 | coverage 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "babel-eslint", 3 | env: { 4 | es2020: true, 5 | node: true, 6 | browser: true 7 | }, 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: 'module' 11 | }, 12 | extends: [ 13 | 'eslint:recommended', 14 | ], 15 | plugins: [ 16 | 'babel', 17 | 'html', 18 | 'svelte3' 19 | ], 20 | overrides: [ 21 | { 22 | files: '**/*.svelte', 23 | processor: 'svelte3/svelte3' 24 | } 25 | ], 26 | settings: { 27 | 'svelte3/ignore-styles': () => true, 28 | 'html': { 29 | 'indent': 0, 30 | 'report-bad-indent': 'warn', 31 | 'html-extensions': [ 32 | '.html' 33 | ] 34 | } 35 | }, 36 | "globals": { 37 | "io": "readonly", 38 | "cy": "readonly", 39 | "context": "readonly", 40 | "beforeEach": "readonly", 41 | "it": "readonly" 42 | }, 43 | rules: { 44 | "strict": 0, 45 | 'indent': [ 46 | 'error', 47 | 2, 48 | { 49 | SwitchCase: 1, 50 | MemberExpression: 1, 51 | ArrayExpression: 1, 52 | ObjectExpression: 1 53 | } 54 | ], 55 | "camelcase": [ 56 | 'warn' 57 | ], 58 | 'linebreak-style': [ 59 | 'error', 60 | 'unix' 61 | ], 62 | 'semi': [ 63 | 'error', 64 | 'never' 65 | ], 66 | // 'quote-props': [ 67 | // 'warn', 68 | // 'as-needed' 69 | // ], 70 | 'no-var': [ 71 | 'error' 72 | ], 73 | 'prefer-const': [ 74 | 'warn' 75 | ], 76 | 'no-unused-vars': [ 77 | 'warn', 78 | { 79 | args: 'none' 80 | } 81 | ], 82 | 'brace-style': [ 83 | 'error', 84 | '1tbs', 85 | { 86 | allowSingleLine: false 87 | } 88 | ], 89 | 'eol-last': [ 90 | 'error', 91 | 'always' 92 | ], 93 | // 'space-before-function-paren': [ 94 | // 'error', 95 | // { 96 | // anonymous: 'never', 97 | // named: 'never', 98 | // asyncArrow: 'always' 99 | // } 100 | // ], 101 | 'keyword-spacing': [ 102 | 'error', 103 | { 104 | before: true, 105 | after: true 106 | } 107 | ], 108 | 'key-spacing': [ 109 | 'error', 110 | { 111 | beforeColon: false, 112 | afterColon: true, 113 | mode: 'strict' 114 | } 115 | ], 116 | 'comma-spacing': [ 117 | 'error' 118 | ], 119 | 'arrow-spacing': [ 120 | 'error' 121 | ], 122 | 'array-bracket-spacing': [ 123 | 'error', 124 | 'never', 125 | { 126 | singleValue: false, 127 | objectsInArrays: true, 128 | arraysInArrays: true 129 | } 130 | ], 131 | 'curly': [ 132 | 'error' 133 | ], 134 | 'space-infix-ops': [ 135 | 'error', 136 | { 137 | int32Hint: false 138 | } 139 | ], 140 | 'space-unary-ops': [ 141 | 'error', 142 | { 143 | words: true, 144 | nonwords: false 145 | } 146 | ], 147 | 'space-before-blocks': [ 148 | 'error' 149 | ], 150 | 'object-curly-spacing': [ 151 | 'error', 152 | 'never' 153 | ], 154 | 'space-in-parens': [ 155 | 'error', 156 | 'never' 157 | ], 158 | 'prefer-arrow-callback': [ 159 | 'warn' 160 | ], 161 | 'no-return-await': [ 162 | 'error' 163 | ], 164 | 'no-console': [ 165 | 'warn' 166 | ], 167 | 'no-nested-ternary': [ 168 | 'error' 169 | ], 170 | 'no-unneeded-ternary': [ 171 | 'warn' 172 | ], 173 | 'no-unexpected-multiline': [ 174 | 'error' 175 | ], 176 | 'lines-around-directive': [ 177 | 'error', 178 | 'always' 179 | ], 180 | 'no-multiple-empty-lines': [ 181 | 'error', 182 | { 183 | max: 2 184 | } 185 | ], 186 | 'operator-linebreak': [ 187 | 'error', 188 | 'after' 189 | ] 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-mango-grass-0e1bab20f.yml: -------------------------------------------------------------------------------- 1 | name: Build Deploy and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: true 22 | 23 | - name: Use Node.js v12.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: '12' 27 | 28 | - name: Build 29 | run: | 30 | npm ci 31 | npm run build 32 | env: 33 | SESSION_SECRET: ${{secrets.SESSION_SECRET}} # TODO: Remove when no longer needed 34 | 35 | - name: Deploy 36 | id: builddeploy 37 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 38 | with: 39 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GRASS_0E1BAB20F }} 40 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 41 | action: "upload" 42 | ###### Repository/Build Configurations - These values can be configured to match you app requirements. ###### 43 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 44 | app_location: "dist" # App source code path 45 | api_location: "api" # Api source code path - optional 46 | # app_artifact_location: "dist" # Built app content directory - optional 47 | ###### End of Repository/Build Configurations ###### 48 | 49 | # e_to_e_test_in_staging_job: 50 | # if: github.event_name == 'pull_request' && github.event.action != 'closed' 51 | # To get this working, I'll need the location of the staged site, which is always 52 | # https://-..azurestaticapps.net 53 | # or the location of the production site which is currently https://mango-grass-0e1bab20f.azurestaticapps.net 54 | # where: 55 | # for MatrX is currently mango-grass-0e1bab20f 56 | # get from the environment variable GITHUB_REF as described here: https://github.com/actions/checkout/issues/58 57 | # is eastus2 58 | # Build the host base URL inside of the cypress tests themselves. Fall back to localhost:7071 if no GITHUB_REF 59 | # 60 | # - name: Cypress run 61 | # uses: cypress-io/github-action@v2 62 | # with: 63 | # build: npm run build --if-present 64 | # start: npm start 65 | # wait-on: http://localhost:8080 66 | # browser: chrome # or firefox or edge with runs-on: windows-latest 67 | # env: 68 | # CYPRESS_GITHUB_REF: ${{github.ref}} # I think this will do the trick 69 | 70 | # smoke_test_in_prod_job: 71 | # if: github.event_name == 'pull_request' && github.event.action == 'closed' 72 | 73 | close_pull_request_job: 74 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 75 | runs-on: ubuntu-latest 76 | name: Close Pull Request Job 77 | steps: 78 | - name: Close Pull Request 79 | id: closepullrequest 80 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 81 | with: 82 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_GRASS_0E1BAB20F }} 83 | action: "close" 84 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning - Action" 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | CodeQL-Build: 10 | 11 | strategy: 12 | fail-fast: false 13 | 14 | 15 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | 22 | # Initializes the CodeQL tools for scanning. 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v1 25 | # Override language selection by uncommenting this and choosing your languages 26 | # with: 27 | # languages: go, javascript, csharp, python, cpp, java 28 | 29 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 30 | # If this step fails, then you should remove it and run the build manually (see below). 31 | - name: Autobuild 32 | uses: github/codeql-action/autobuild@v1 33 | 34 | # ℹ️ Command-line programs to run using the OS shell. 35 | # 📚 https://git.io/JvXDl 36 | 37 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 38 | # and modify them (or add more) to build your code if your project 39 | # uses a compiled language 40 | 41 | #- run: | 42 | # make bootstrap 43 | # make release 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v1 47 | -------------------------------------------------------------------------------- /.github/workflows/coverage-base-on-master.yml.upgrade-to-remove-express: -------------------------------------------------------------------------------- 1 | name: Coverage base on master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | coverage-base-on-master: 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: npm ci, build, and coverage testing 27 | run: | 28 | npm ci 29 | npm run css:build 30 | npm run coverage 31 | env: 32 | CI: true 33 | SESSION_SECRET: ${{secrets.SESSION_SECRET}} 34 | 35 | - uses: codecov/codecov-action@v1.0.3 36 | with: 37 | token: ${{secrets.CODECOV_TOKEN}} 38 | # file: ./coverage.xml #optional 39 | 40 | - name: upload html report as artifact 41 | uses: actions/upload-artifact@master 42 | with: 43 | name: coverage-report 44 | path: coverage/lcov-report 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml.upgrade-to-remove-express: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | coverage: 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | os: [ubuntu-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: npm ci, build, and coverage testing 27 | run: | 28 | npm ci 29 | npm run css:build 30 | npm run coverage 31 | # npm run coverage:check 32 | env: 33 | CI: true 34 | SESSION_SECRET: ${{secrets.SESSION_SECRET}} 35 | 36 | - uses: codecov/codecov-action@v1.0.3 37 | with: 38 | token: ${{secrets.CODECOV_TOKEN}} 39 | # file: ./coverage.xml #optional 40 | 41 | - name: upload html report as artifact 42 | uses: actions/upload-artifact@master 43 | with: 44 | name: coverage-report 45 | path: coverage/lcov-report 46 | 47 | - name: check coverage levels 48 | run: | 49 | npm run coverage:check 50 | env: 51 | CI: true 52 | -------------------------------------------------------------------------------- /.github/workflows/cypress-tests.yml.upgrade-to-remove-express: -------------------------------------------------------------------------------- 1 | name: Cypress tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | cypress: 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | matrix: 15 | # node-version: [13.x, 12.x, 10.x] 16 | node-version: [12.x] 17 | os: [ubuntu-latest] 18 | # os: [ubuntu-latest, macOS-latest] 19 | # os: [windows-latest, ubuntu-latest, macOS-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | # - name: npm install and build 29 | # run: | 30 | # npm ci 31 | # npm run build --if-present 32 | # npm run css:build 33 | # npm test 34 | # env: 35 | # CI: true 36 | # SESSION_SECRET: ${{secrets.SESSION_SECRET}} 37 | 38 | - name: Cypress run 39 | uses: cypress-io/github-action@v1 40 | with: 41 | build: npm run build --if-present 42 | start: npm start 43 | wait-on: http://localhost:8080 44 | env: 45 | CI: true 46 | SESSION_SECRET: ${{secrets.SESSION_SECRET}} 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | lint: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm ci 26 | npm run lint 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dist 2 | dist/bundle* 3 | dist/global.css 4 | 5 | # Coverage 6 | coverage/ 7 | coverage-server/ 8 | 9 | # Cypress 10 | cypress/videos 11 | cypress/screenshots 12 | 13 | ### Linux ### 14 | *~ 15 | 16 | # temporary files which can be created if a process still has a handle open of a deleted file 17 | .fuse_hidden* 18 | 19 | # KDE directory preferences 20 | .directory 21 | 22 | # Linux trash folder which might appear on any partition or disk 23 | .Trash-* 24 | 25 | # .nfs files are created when an open file is removed but is still being accessed 26 | .nfs* 27 | 28 | ### macOS ### 29 | # General 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | 46 | # Diagnostic reports (https://nodejs.org/api/report.html) 47 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Directory for instrumented libs generated by jscoverage/JSCover 56 | lib-cov 57 | 58 | # Coverage directory used by tools like istanbul 59 | coverage 60 | 61 | # nyc test coverage 62 | .nyc_output 63 | 64 | # Dependency directories 65 | node_modules/ 66 | 67 | # Optional npm cache directory 68 | .npm 69 | 70 | # Optional eslint cache 71 | .eslintcache 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | 83 | ### VisualStudioCode ### 84 | .vscode/* 85 | !.vscode/settings.json 86 | !.vscode/tasks.json 87 | !.vscode/launch.json 88 | !.vscode/extensions.json 89 | 90 | ### VisualStudioCode Patch ### 91 | # Ignore all local history of files 92 | .history 93 | dist/global.css 94 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "html", 5 | "svelte" 6 | ], 7 | "editor.detectIndentation": false, 8 | "editor.insertSpaces": true, 9 | "editor.tabSize": 2, 10 | "search.exclude": { 11 | "**/node_modules": true, 12 | "dist/": true 13 | }, 14 | "files.exclude": { 15 | "**/node_modules": true 16 | }, 17 | "[html]": { 18 | "editor.tabSize": 2 19 | }, 20 | "[svelte]": { 21 | "editor.tabSize": 2 22 | }, 23 | "[scss]": { 24 | "editor.tabSize": 2 25 | }, 26 | "[yaml]": { 27 | "editor.tabSize": 2 28 | }, 29 | "[javascript]": { 30 | "editor.tabSize": 2 31 | }, 32 | "[json]": { 33 | "editor.tabSize": 2 34 | }, 35 | "azureFunctions.deploySubpath": "api", 36 | "azureFunctions.postDeployTask": "npm install", 37 | "azureFunctions.projectLanguage": "JavaScript", 38 | "azureFunctions.projectRuntime": "~3", 39 | "debug.internalConsoleOptions": "neverOpen", 40 | "azureFunctions.preDeployTask": "npm prune" 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm install", 10 | "options": { 11 | "cwd": "${workspaceFolder}/api" 12 | } 13 | }, 14 | { 15 | "type": "shell", 16 | "label": "npm install", 17 | "command": "npm install", 18 | "options": { 19 | "cwd": "${workspaceFolder}/api" 20 | } 21 | }, 22 | { 23 | "type": "shell", 24 | "label": "npm prune", 25 | "command": "npm prune --production", 26 | "problemMatcher": [], 27 | "options": { 28 | "cwd": "${workspaceFolder}/api" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Larry Maccherone 2 | 3 | All of the components found in the packages folder are open sourced under the MIT license but this main project is not open source licensed... eventhough the source is currently "open". For now, feel free to look at the code in this main project as an example for how to use the open sourced components and of course, we'd love for you to use the open sourced components. Please contact us if you wish to use this parent project for commercial purposes. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Azure Static Web App GitHub Actions Workflow](https://img.shields.io/github/workflow/status/matrx-transformation/matrx/Build%20Deploy%20and%20Test?label=Build%20Deploy%20and%20Test&style=for-the-badge) 2 | 3 | ![](https://img.shields.io/github/workflow/status/matrx-transformation/matrx/Lint?label=Lint&style=for-the-badge) 4 | 5 | ![Codecov](https://img.shields.io/codecov/c/github/matrx-transformation/matrx?style=for-the-badge) 6 | 7 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/matrx-transformation/matrx.svg?logo=lgtm&logoWidth=18&style=for-the-badge)](https://lgtm.com/projects/g/matrx-transformation/matrx/alerts/) 8 | 9 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/matrx-transformation/matrx.svg?logo=lgtm&logoWidth=18&style=for-the-badge)](https://lgtm.com/projects/g/matrx-transformation/matrx/context:javascript) 10 | 11 | 12 | ![Azure DevOps releases](https://img.shields.io/azure-devops/release/matrx-transformation/eeebab5f-7e66-4a67-bd67-c98a299f2aae/1/1?style=for-the-badge) 13 | 14 | # `@matrx/matrx` 15 | 16 | This is the repository for the [MatrX](https://matrx.co) product. 17 | 18 | ## License 19 | 20 | All of the components found in the packages folder are open sourced under the MIT license but this main project is not open source licensed... eventhough the source is currently "open". Right now, it's current purpose is to provide end-to-end testing for the open sourced components. Feel free to look at the code in this main project as an example for how to use the open sourced components and of course, we'd love for you to use the open sourced components. Please contact us if you wish to use this parent project for commercial purposes. 21 | -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json -------------------------------------------------------------------------------- /api/context/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /api/context/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (context, req) { 2 | const assigneeID = process.env.WEBSITE_STATICWEBAPP_FUNCTION_ASSIGNEEID 3 | let environment 4 | if (assigneeID) { 5 | if (assigneeID.includes('app/master')) { 6 | environment = 'production' 7 | } else { 8 | environment = 'staging' 9 | } 10 | } else { 11 | environment = 'development' 12 | } 13 | 14 | context.res = { 15 | // status: 200, /* Defaults to 200 */ 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | }, 19 | // body: {nodeEnv: process.env.NODE_ENV, serverEnvironment} 20 | body: {environment, requestObject: context.req} 21 | } 22 | 23 | // const name = req.query.name || (req.body && req.body.name) 24 | // if (name) { 25 | // context.res = { 26 | // // status: 200, /* Defaults to 200 */ 27 | // headers: { 28 | // 'Content-Type': 'application/json' 29 | // }, 30 | // body: {name} 31 | // } 32 | // } else { 33 | // context.res = { 34 | // status: 400, 35 | // body: "Please pass a name on the query string for GET or in the request body as a POST" 36 | // } 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /api/dist-proxy/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "res" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /api/dist-proxy/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require('path') 3 | const mime = require('mime-types') 4 | 5 | module.exports = function (context, req) { 6 | 7 | let file = "index.html" 8 | 9 | if (req.query.file) { 10 | file = req.query.file 11 | } 12 | 13 | const fullPath = path.join(__dirname, '..', '..', 'dist', file) 14 | 15 | fs.readFile(fullPath, (err, data) => { 16 | 17 | context.log('GET ' + "/" + file) 18 | 19 | if (!err) { 20 | 21 | const contentType = mime.lookup(file) 22 | 23 | context.res = { 24 | status: 200, 25 | body: data, 26 | isRaw: true, 27 | headers: { 28 | 'Content-Type': contentType 29 | } 30 | } 31 | } else { 32 | 33 | context.log("Error: " + err) 34 | 35 | context.res = { 36 | status: 404, 37 | body: "Not Found.", 38 | headers: { 39 | } 40 | } 41 | } 42 | context.done() 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "functionTimeout": "00:05:00", 4 | "logging": { 5 | "logLevel": { 6 | "default": "Debug" 7 | }, 8 | "applicationInsights": { 9 | "samplingSettings": { 10 | "isEnabled": true, 11 | "excludedTypes": "Request" 12 | } 13 | } 14 | }, 15 | "extensionBundle": { 16 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 17 | "version": "[1.*, 2.0.0)" 18 | } 19 | } -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "func start --enableAuth", 7 | "test": "echo \"No tests yet...\"" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "azure-functions-core-tools": "^3.0.2630", 12 | "mime-types": "^2.1.27" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": { 4 | "theAPI": { 5 | "matchCondition": { 6 | "methods": [ 7 | "GET" 8 | ], 9 | "route": "/api/{*restOfPath}" 10 | }, 11 | "backendUri": "http://localhost:7071/api/{restOfPath}" 12 | }, 13 | "root": { 14 | "matchCondition": { 15 | "methods": [ 16 | "GET" 17 | ], 18 | "route": "/{*restOfPath}" 19 | }, 20 | "backendUri": "http://localhost:7071/api/dist-proxy?file={restOfPath}" 21 | }, 22 | "root-index": { 23 | "matchCondition": { 24 | "methods": [ 25 | "GET" 26 | ], 27 | "route": "/" 28 | }, 29 | "backendUri": "http://localhost:7071/api/dist-proxy?file=index.html" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /api/status/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "res" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /api/status/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (context, req) { 2 | const name = req.query.name || (req.body && req.body.name) 3 | if (name) { 4 | context.res = { 5 | // status: 200, /* Defaults to 200 */ 6 | headers: { 7 | 'Content-Type': 'application/json' 8 | }, 9 | body: {name} 10 | } 11 | } else { 12 | context.res = { 13 | status: 400, 14 | body: {error: "Please pass a name on the query string for GET or in the request body as a POST"} 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "edge": "17", 8 | "firefox": "60", 9 | "chrome": "67", 10 | "safari": "11.1" 11 | }, 12 | "useBuiltIns": "usage", 13 | "corejs": "3.6.4" 14 | } 15 | ] 16 | ] 17 | } -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "defaultCommandTimeout": 3000 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/.examples/aliasing.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Aliasing', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/aliasing') 6 | }) 7 | 8 | it('.as() - alias a DOM element for later use', () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get('.as-table').find('tbody>tr') 16 | .first().find('td').first() 17 | .find('button').as('firstBtn') 18 | 19 | // when we reference the alias, we place an 20 | // @ in front of its name 21 | cy.get('@firstBtn').click() 22 | 23 | cy.get('@firstBtn') 24 | .should('have.class', 'btn-success') 25 | .and('contain', 'Changed') 26 | }) 27 | 28 | it('.as() - alias a route for later use', () => { 29 | 30 | // Alias the route to wait for its response 31 | cy.server() 32 | cy.route('GET', 'comments/*').as('getComment') 33 | 34 | // we have code that gets a comment when 35 | // the button is clicked in scripts.js 36 | cy.get('.network-btn').click() 37 | 38 | // https://on.cypress.io/wait 39 | cy.wait('@getComment').its('status').should('eq', 200) 40 | 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /cypress/integration/.examples/assertions.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Assertions', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/assertions') 6 | }) 7 | 8 | describe('Implicit Assertions', () => { 9 | it('.should() - make an assertion about the current subject', () => { 10 | // https://on.cypress.io/should 11 | cy.get('.assertion-table') 12 | .find('tbody tr:last') 13 | .should('have.class', 'success') 14 | .find('td') 15 | .first() 16 | // checking the text of the element in various ways 17 | .should('have.text', 'Column content') 18 | .should('contain', 'Column content') 19 | .should('have.html', 'Column content') 20 | // chai-jquery uses "is()" to check if element matches selector 21 | .should('match', 'td') 22 | // to match text content against a regular expression 23 | // first need to invoke jQuery method text() 24 | // and then match using regular expression 25 | .invoke('text') 26 | .should('match', /column content/i) 27 | 28 | // a better way to check element's text content against a regular expression 29 | // is to use "cy.contains" 30 | // https://on.cypress.io/contains 31 | cy.get('.assertion-table') 32 | .find('tbody tr:last') 33 | // finds first element with text content matching regular expression 34 | .contains('td', /column content/i) 35 | .should('be.visible') 36 | 37 | // for more information about asserting element's text 38 | // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents 39 | }) 40 | 41 | it('.and() - chain multiple assertions together', () => { 42 | // https://on.cypress.io/and 43 | cy.get('.assertions-link') 44 | .should('have.class', 'active') 45 | .and('have.attr', 'href') 46 | .and('include', 'cypress.io') 47 | }) 48 | }) 49 | 50 | describe('Explicit Assertions', () => { 51 | // https://on.cypress.io/assertions 52 | it('expect - make an assertion about a specified subject', () => { 53 | // We can use Chai's BDD style assertions 54 | expect(true).to.be.true 55 | const o = { foo: 'bar' } 56 | 57 | expect(o).to.equal(o) 58 | expect(o).to.deep.equal({ foo: 'bar' }) 59 | // matching text using regular expression 60 | expect('FooBar').to.match(/bar$/i) 61 | }) 62 | 63 | it('pass your own callback function to should()', () => { 64 | // Pass a function to should that can have any number 65 | // of explicit assertions within it. 66 | // The ".should(cb)" function will be retried 67 | // automatically until it passes all your explicit assertions or times out. 68 | cy.get('.assertions-p') 69 | .find('p') 70 | .should(($p) => { 71 | // https://on.cypress.io/$ 72 | // return an array of texts from all of the p's 73 | // @ts-ignore TS6133 unused variable 74 | const texts = $p.map((i, el) => Cypress.$(el).text()) 75 | 76 | // jquery map returns jquery object 77 | // and .get() convert this to simple array 78 | const paragraphs = texts.get() 79 | 80 | // array should have length of 3 81 | expect(paragraphs, 'has 3 paragraphs').to.have.length(3) 82 | 83 | // use second argument to expect(...) to provide clear 84 | // message with each assertion 85 | expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ 86 | 'Some text from first p', 87 | 'More text from second p', 88 | 'And even more text from third p', 89 | ]) 90 | }) 91 | }) 92 | 93 | it('finds element by class name regex', () => { 94 | cy.get('.docs-header') 95 | .find('div') 96 | // .should(cb) callback function will be retried 97 | .should(($div) => { 98 | expect($div).to.have.length(1) 99 | 100 | const className = $div[0].className 101 | 102 | expect(className).to.match(/heading-/) 103 | }) 104 | // .then(cb) callback is not retried, 105 | // it either passes or fails 106 | .then(($div) => { 107 | expect($div, 'text content').to.have.text('Introduction') 108 | }) 109 | }) 110 | 111 | it('can throw any error', () => { 112 | cy.get('.docs-header') 113 | .find('div') 114 | .should(($div) => { 115 | if ($div.length !== 1) { 116 | // you can throw your own errors 117 | throw new Error('Did not find 1 element') 118 | } 119 | 120 | const className = $div[0].className 121 | 122 | if (!className.match(/heading-/)) { 123 | throw new Error(`Could not find class "heading-" in ${className}`) 124 | } 125 | }) 126 | }) 127 | 128 | it('matches unknown text between two elements', () => { 129 | /** 130 | * Text from the first element. 131 | * @type {string} 132 | */ 133 | let text 134 | 135 | /** 136 | * Normalizes passed text, 137 | * useful before comparing text with spaces and different capitalization. 138 | * @param {string} s Text to normalize 139 | */ 140 | const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() 141 | 142 | cy.get('.two-elements') 143 | .find('.first') 144 | .then(($first) => { 145 | // save text from the first element 146 | text = normalizeText($first.text()) 147 | }) 148 | 149 | cy.get('.two-elements') 150 | .find('.second') 151 | .should(($div) => { 152 | // we can massage text before comparing 153 | const secondText = normalizeText($div.text()) 154 | 155 | expect(secondText, 'second text').to.equal(text) 156 | }) 157 | }) 158 | 159 | it('assert - assert shape of an object', () => { 160 | const person = { 161 | name: 'Joe', 162 | age: 20, 163 | } 164 | 165 | assert.isObject(person, 'value is object') 166 | }) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /cypress/integration/.examples/connectors.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Connectors', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/connectors') 6 | }) 7 | 8 | it('.each() - iterate over an array of elements', () => { 9 | // https://on.cypress.io/each 10 | cy.get('.connectors-each-ul>li') 11 | .each(($el, index, $list) => { 12 | console.log($el, index, $list) 13 | }) 14 | }) 15 | 16 | it('.its() - get properties on the current subject', () => { 17 | // https://on.cypress.io/its 18 | cy.get('.connectors-its-ul>li') 19 | // calls the 'length' property yielding that value 20 | .its('length') 21 | .should('be.gt', 2) 22 | }) 23 | 24 | it('.invoke() - invoke a function on the current subject', () => { 25 | // our div is hidden in our script.js 26 | // $('.connectors-div').hide() 27 | 28 | // https://on.cypress.io/invoke 29 | cy.get('.connectors-div').should('be.hidden') 30 | // call the jquery method 'show' on the 'div.container' 31 | .invoke('show') 32 | .should('be.visible') 33 | }) 34 | 35 | it('.spread() - spread an array as individual args to callback function', () => { 36 | // https://on.cypress.io/spread 37 | const arr = ['foo', 'bar', 'baz'] 38 | 39 | cy.wrap(arr).spread((foo, bar, baz) => { 40 | expect(foo).to.eq('foo') 41 | expect(bar).to.eq('bar') 42 | expect(baz).to.eq('baz') 43 | }) 44 | }) 45 | 46 | it('.then() - invoke a callback function with the current subject', () => { 47 | // https://on.cypress.io/then 48 | cy.get('.connectors-list > li') 49 | .then(($lis) => { 50 | expect($lis, '3 items').to.have.length(3) 51 | expect($lis.eq(0), 'first item').to.contain('Walk the dog') 52 | expect($lis.eq(1), 'second item').to.contain('Feed the cat') 53 | expect($lis.eq(2), 'third item').to.contain('Write JavaScript') 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /cypress/integration/.examples/cookies.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Cookies', () => { 4 | beforeEach(() => { 5 | Cypress.Cookies.debug(true) 6 | 7 | cy.visit('https://example.cypress.io/commands/cookies') 8 | 9 | // clear cookies again after visiting to remove 10 | // any 3rd party cookies picked up such as cloudflare 11 | cy.clearCookies() 12 | }) 13 | 14 | it('cy.getCookie() - get a browser cookie', () => { 15 | // https://on.cypress.io/getcookie 16 | cy.get('#getCookie .set-a-cookie').click() 17 | 18 | // cy.getCookie() yields a cookie object 19 | cy.getCookie('token').should('have.property', 'value', '123ABC') 20 | }) 21 | 22 | it('cy.getCookies() - get browser cookies', () => { 23 | // https://on.cypress.io/getcookies 24 | cy.getCookies().should('be.empty') 25 | 26 | cy.get('#getCookies .set-a-cookie').click() 27 | 28 | // cy.getCookies() yields an array of cookies 29 | cy.getCookies().should('have.length', 1).should((cookies) => { 30 | 31 | // each cookie has these properties 32 | expect(cookies[0]).to.have.property('name', 'token') 33 | expect(cookies[0]).to.have.property('value', '123ABC') 34 | expect(cookies[0]).to.have.property('httpOnly', false) 35 | expect(cookies[0]).to.have.property('secure', false) 36 | expect(cookies[0]).to.have.property('domain') 37 | expect(cookies[0]).to.have.property('path') 38 | }) 39 | }) 40 | 41 | it('cy.setCookie() - set a browser cookie', () => { 42 | // https://on.cypress.io/setcookie 43 | cy.getCookies().should('be.empty') 44 | 45 | cy.setCookie('foo', 'bar') 46 | 47 | // cy.getCookie() yields a cookie object 48 | cy.getCookie('foo').should('have.property', 'value', 'bar') 49 | }) 50 | 51 | it('cy.clearCookie() - clear a browser cookie', () => { 52 | // https://on.cypress.io/clearcookie 53 | cy.getCookie('token').should('be.null') 54 | 55 | cy.get('#clearCookie .set-a-cookie').click() 56 | 57 | cy.getCookie('token').should('have.property', 'value', '123ABC') 58 | 59 | // cy.clearCookies() yields null 60 | cy.clearCookie('token').should('be.null') 61 | 62 | cy.getCookie('token').should('be.null') 63 | }) 64 | 65 | it('cy.clearCookies() - clear browser cookies', () => { 66 | // https://on.cypress.io/clearcookies 67 | cy.getCookies().should('be.empty') 68 | 69 | cy.get('#clearCookies .set-a-cookie').click() 70 | 71 | cy.getCookies().should('have.length', 1) 72 | 73 | // cy.clearCookies() yields null 74 | cy.clearCookies() 75 | 76 | cy.getCookies().should('be.empty') 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /cypress/integration/.examples/cypress_api.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Cypress.Commands', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/cypress-api') 6 | }) 7 | 8 | // https://on.cypress.io/custom-commands 9 | 10 | it('.add() - create a custom command', () => { 11 | Cypress.Commands.add('console', { 12 | prevSubject: true, 13 | }, (subject, method) => { 14 | // the previous subject is automatically received 15 | // and the commands arguments are shifted 16 | 17 | // allow us to change the console method used 18 | method = method || 'log' 19 | 20 | // log the subject to the console 21 | // @ts-ignore TS7017 22 | console[method]('The subject is', subject) 23 | 24 | // whatever we return becomes the new subject 25 | // we don't want to change the subject so 26 | // we return whatever was passed in 27 | return subject 28 | }) 29 | 30 | // @ts-ignore TS2339 31 | cy.get('button').console('info').then(($button) => { 32 | // subject is still $button 33 | }) 34 | }) 35 | }) 36 | 37 | 38 | context('Cypress.Cookies', () => { 39 | beforeEach(() => { 40 | cy.visit('https://example.cypress.io/cypress-api') 41 | }) 42 | 43 | // https://on.cypress.io/cookies 44 | it('.debug() - enable or disable debugging', () => { 45 | Cypress.Cookies.debug(true) 46 | 47 | // Cypress will now log in the console when 48 | // cookies are set or cleared 49 | cy.setCookie('fakeCookie', '123ABC') 50 | cy.clearCookie('fakeCookie') 51 | cy.setCookie('fakeCookie', '123ABC') 52 | cy.clearCookie('fakeCookie') 53 | cy.setCookie('fakeCookie', '123ABC') 54 | }) 55 | 56 | it('.preserveOnce() - preserve cookies by key', () => { 57 | // normally cookies are reset after each test 58 | cy.getCookie('fakeCookie').should('not.be.ok') 59 | 60 | // preserving a cookie will not clear it when 61 | // the next test starts 62 | cy.setCookie('lastCookie', '789XYZ') 63 | Cypress.Cookies.preserveOnce('lastCookie') 64 | }) 65 | 66 | it('.defaults() - set defaults for all cookies', () => { 67 | // now any cookie with the name 'session_id' will 68 | // not be cleared before each new test runs 69 | Cypress.Cookies.defaults({ 70 | whitelist: 'session_id', 71 | }) 72 | }) 73 | }) 74 | 75 | 76 | context('Cypress.Server', () => { 77 | beforeEach(() => { 78 | cy.visit('https://example.cypress.io/cypress-api') 79 | }) 80 | 81 | // Permanently override server options for 82 | // all instances of cy.server() 83 | 84 | // https://on.cypress.io/cypress-server 85 | it('.defaults() - change default config of server', () => { 86 | Cypress.Server.defaults({ 87 | delay: 0, 88 | force404: false, 89 | }) 90 | }) 91 | }) 92 | 93 | context('Cypress.arch', () => { 94 | beforeEach(() => { 95 | cy.visit('https://example.cypress.io/cypress-api') 96 | }) 97 | 98 | it('Get CPU architecture name of underlying OS', () => { 99 | // https://on.cypress.io/arch 100 | expect(Cypress.arch).to.exist 101 | }) 102 | }) 103 | 104 | context('Cypress.config()', () => { 105 | beforeEach(() => { 106 | cy.visit('https://example.cypress.io/cypress-api') 107 | }) 108 | 109 | it('Get and set configuration options', () => { 110 | // https://on.cypress.io/config 111 | let myConfig = Cypress.config() 112 | 113 | expect(myConfig).to.have.property('animationDistanceThreshold', 5) 114 | expect(myConfig).to.have.property('baseUrl', null) 115 | expect(myConfig).to.have.property('defaultCommandTimeout', 4000) 116 | expect(myConfig).to.have.property('requestTimeout', 5000) 117 | expect(myConfig).to.have.property('responseTimeout', 30000) 118 | expect(myConfig).to.have.property('viewportHeight', 660) 119 | expect(myConfig).to.have.property('viewportWidth', 1000) 120 | expect(myConfig).to.have.property('pageLoadTimeout', 60000) 121 | expect(myConfig).to.have.property('waitForAnimations', true) 122 | 123 | expect(Cypress.config('pageLoadTimeout')).to.eq(60000) 124 | 125 | // this will change the config for the rest of your tests! 126 | Cypress.config('pageLoadTimeout', 20000) 127 | 128 | expect(Cypress.config('pageLoadTimeout')).to.eq(20000) 129 | 130 | Cypress.config('pageLoadTimeout', 60000) 131 | }) 132 | }) 133 | 134 | context('Cypress.dom', () => { 135 | beforeEach(() => { 136 | cy.visit('https://example.cypress.io/cypress-api') 137 | }) 138 | 139 | // https://on.cypress.io/dom 140 | it('.isHidden() - determine if a DOM element is hidden', () => { 141 | let hiddenP = Cypress.$('.dom-p p.hidden').get(0) 142 | let visibleP = Cypress.$('.dom-p p.visible').get(0) 143 | 144 | // our first paragraph has css class 'hidden' 145 | expect(Cypress.dom.isHidden(hiddenP)).to.be.true 146 | expect(Cypress.dom.isHidden(visibleP)).to.be.false 147 | }) 148 | }) 149 | 150 | context('Cypress.env()', () => { 151 | beforeEach(() => { 152 | cy.visit('https://example.cypress.io/cypress-api') 153 | }) 154 | 155 | // We can set environment variables for highly dynamic values 156 | 157 | // https://on.cypress.io/environment-variables 158 | it('Get environment variables', () => { 159 | // https://on.cypress.io/env 160 | // set multiple environment variables 161 | Cypress.env({ 162 | host: 'veronica.dev.local', 163 | api_server: 'http://localhost:8888/v1/', 164 | }) 165 | 166 | // get environment variable 167 | expect(Cypress.env('host')).to.eq('veronica.dev.local') 168 | 169 | // set environment variable 170 | Cypress.env('api_server', 'http://localhost:8888/v2/') 171 | expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') 172 | 173 | // get all environment variable 174 | expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') 175 | expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') 176 | }) 177 | }) 178 | 179 | context('Cypress.log', () => { 180 | beforeEach(() => { 181 | cy.visit('https://example.cypress.io/cypress-api') 182 | }) 183 | 184 | it('Control what is printed to the Command Log', () => { 185 | // https://on.cypress.io/cypress-log 186 | }) 187 | }) 188 | 189 | 190 | context('Cypress.platform', () => { 191 | beforeEach(() => { 192 | cy.visit('https://example.cypress.io/cypress-api') 193 | }) 194 | 195 | it('Get underlying OS name', () => { 196 | // https://on.cypress.io/platform 197 | expect(Cypress.platform).to.be.exist 198 | }) 199 | }) 200 | 201 | context('Cypress.version', () => { 202 | beforeEach(() => { 203 | cy.visit('https://example.cypress.io/cypress-api') 204 | }) 205 | 206 | it('Get current version of Cypress being run', () => { 207 | // https://on.cypress.io/version 208 | expect(Cypress.version).to.be.exist 209 | }) 210 | }) 211 | 212 | context('Cypress.spec', () => { 213 | beforeEach(() => { 214 | cy.visit('https://example.cypress.io/cypress-api') 215 | }) 216 | 217 | it('Get current spec information', () => { 218 | // https://on.cypress.io/spec 219 | // wrap the object so we can inspect it easily by clicking in the command log 220 | cy.wrap(Cypress.spec).should('have.keys', ['name', 'relative', 'absolute']) 221 | }) 222 | }) 223 | -------------------------------------------------------------------------------- /cypress/integration/.examples/files.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /// JSON fixture file can be loaded directly using 4 | // the built-in JavaScript bundler 5 | // @ts-ignore 6 | const requiredExample = require('../../fixtures/example') 7 | 8 | context('Files', () => { 9 | beforeEach(() => { 10 | cy.visit('https://example.cypress.io/commands/files') 11 | }) 12 | 13 | beforeEach(() => { 14 | // load example.json fixture file and store 15 | // in the test context object 16 | cy.fixture('example.json').as('example') 17 | }) 18 | 19 | it('cy.fixture() - load a fixture', () => { 20 | // https://on.cypress.io/fixture 21 | 22 | // Instead of writing a response inline you can 23 | // use a fixture file's content. 24 | 25 | cy.server() 26 | cy.fixture('example.json').as('comment') 27 | // when application makes an Ajax request matching "GET comments/*" 28 | // Cypress will intercept it and reply with object 29 | // from the "comment" alias 30 | cy.route('GET', 'comments/*', '@comment').as('getComment') 31 | 32 | // we have code that gets a comment when 33 | // the button is clicked in scripts.js 34 | cy.get('.fixture-btn').click() 35 | 36 | cy.wait('@getComment').its('responseBody') 37 | .should('have.property', 'name') 38 | .and('include', 'Using fixtures to represent data') 39 | 40 | // you can also just write the fixture in the route 41 | cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment') 42 | 43 | // we have code that gets a comment when 44 | // the button is clicked in scripts.js 45 | cy.get('.fixture-btn').click() 46 | 47 | cy.wait('@getComment').its('responseBody') 48 | .should('have.property', 'name') 49 | .and('include', 'Using fixtures to represent data') 50 | 51 | // or write fx to represent fixture 52 | // by default it assumes it's .json 53 | cy.route('GET', 'comments/*', 'fx:example').as('getComment') 54 | 55 | // we have code that gets a comment when 56 | // the button is clicked in scripts.js 57 | cy.get('.fixture-btn').click() 58 | 59 | cy.wait('@getComment').its('responseBody') 60 | .should('have.property', 'name') 61 | .and('include', 'Using fixtures to represent data') 62 | }) 63 | 64 | it('cy.fixture() or require - load a fixture', function () { 65 | // we are inside the "function () { ... }" 66 | // callback and can use test context object "this" 67 | // "this.example" was loaded in "beforeEach" function callback 68 | expect(this.example, 'fixture in the test context') 69 | .to.deep.equal(requiredExample) 70 | 71 | // or use "cy.wrap" and "should('deep.equal', ...)" assertion 72 | // @ts-ignore 73 | cy.wrap(this.example, 'fixture vs require') 74 | .should('deep.equal', requiredExample) 75 | }) 76 | 77 | it('cy.readFile() - read a files contents', () => { 78 | // https://on.cypress.io/readfile 79 | 80 | // You can read a file and yield its contents 81 | // The filePath is relative to your project's root. 82 | cy.readFile('cypress.json').then((json) => { 83 | expect(json).to.be.an('object') 84 | }) 85 | }) 86 | 87 | it('cy.writeFile() - write to a file', () => { 88 | // https://on.cypress.io/writefile 89 | 90 | // You can write to a file 91 | 92 | // Use a response from a request to automatically 93 | // generate a fixture file for use later 94 | cy.request('https://jsonplaceholder.cypress.io/users') 95 | .then((response) => { 96 | cy.writeFile('cypress/fixtures/users.json', response.body) 97 | }) 98 | cy.fixture('users').should((users) => { 99 | expect(users[0].name).to.exist 100 | }) 101 | 102 | // JavaScript arrays and objects are stringified 103 | // and formatted into text. 104 | cy.writeFile('cypress/fixtures/profile.json', { 105 | id: 8739, 106 | name: 'Jane', 107 | email: 'jane@example.com', 108 | }) 109 | 110 | cy.fixture('profile').should((profile) => { 111 | expect(profile.name).to.eq('Jane') 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /cypress/integration/.examples/local_storage.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Local Storage', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/local-storage') 6 | }) 7 | // Although local storage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear the local storage manually 10 | 11 | it('cy.clearLocalStorage() - clear all data in local storage', () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get('.ls-btn').click().should(() => { 14 | expect(localStorage.getItem('prop1')).to.eq('red') 15 | expect(localStorage.getItem('prop2')).to.eq('blue') 16 | expect(localStorage.getItem('prop3')).to.eq('magenta') 17 | }) 18 | 19 | // clearLocalStorage() yields the localStorage object 20 | cy.clearLocalStorage().should((ls) => { 21 | expect(ls.getItem('prop1')).to.be.null 22 | expect(ls.getItem('prop2')).to.be.null 23 | expect(ls.getItem('prop3')).to.be.null 24 | }) 25 | 26 | // Clear key matching string in Local Storage 27 | cy.get('.ls-btn').click().should(() => { 28 | expect(localStorage.getItem('prop1')).to.eq('red') 29 | expect(localStorage.getItem('prop2')).to.eq('blue') 30 | expect(localStorage.getItem('prop3')).to.eq('magenta') 31 | }) 32 | 33 | cy.clearLocalStorage('prop1').should((ls) => { 34 | expect(ls.getItem('prop1')).to.be.null 35 | expect(ls.getItem('prop2')).to.eq('blue') 36 | expect(ls.getItem('prop3')).to.eq('magenta') 37 | }) 38 | 39 | // Clear keys matching regex in Local Storage 40 | cy.get('.ls-btn').click().should(() => { 41 | expect(localStorage.getItem('prop1')).to.eq('red') 42 | expect(localStorage.getItem('prop2')).to.eq('blue') 43 | expect(localStorage.getItem('prop3')).to.eq('magenta') 44 | }) 45 | 46 | cy.clearLocalStorage(/prop1|2/).should((ls) => { 47 | expect(ls.getItem('prop1')).to.be.null 48 | expect(ls.getItem('prop2')).to.be.null 49 | expect(ls.getItem('prop3')).to.eq('magenta') 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /cypress/integration/.examples/location.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Location', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/location') 6 | }) 7 | 8 | it('cy.hash() - get the current URL hash', () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should('be.empty') 11 | }) 12 | 13 | it('cy.location() - get window.location', () => { 14 | // https://on.cypress.io/location 15 | cy.location().should((location) => { 16 | expect(location.hash).to.be.empty 17 | expect(location.href).to.eq('https://example.cypress.io/commands/location') 18 | expect(location.host).to.eq('example.cypress.io') 19 | expect(location.hostname).to.eq('example.cypress.io') 20 | expect(location.origin).to.eq('https://example.cypress.io') 21 | expect(location.pathname).to.eq('/commands/location') 22 | expect(location.port).to.eq('') 23 | expect(location.protocol).to.eq('https:') 24 | expect(location.search).to.be.empty 25 | }) 26 | }) 27 | 28 | it('cy.url() - get the current URL', () => { 29 | // https://on.cypress.io/url 30 | cy.url().should('eq', 'https://example.cypress.io/commands/location') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/integration/.examples/misc.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Misc', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/misc') 6 | }) 7 | 8 | it('.end() - end the command chain', () => { 9 | // https://on.cypress.io/end 10 | 11 | // cy.end is useful when you want to end a chain of commands 12 | // and force Cypress to re-query from the root element 13 | cy.get('.misc-table').within(() => { 14 | // ends the current chain and yields null 15 | cy.contains('Cheryl').click().end() 16 | 17 | // queries the entire table again 18 | cy.contains('Charles').click() 19 | }) 20 | }) 21 | 22 | it('cy.exec() - execute a system command', () => { 23 | // https://on.cypress.io/exec 24 | 25 | // execute a system command. 26 | // so you can take actions necessary for 27 | // your test outside the scope of Cypress. 28 | cy.exec('echo Jane Lane') 29 | .its('stdout').should('contain', 'Jane Lane') 30 | 31 | // we can use Cypress.platform string to 32 | // select appropriate command 33 | // https://on.cypress/io/platform 34 | cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) 35 | 36 | if (Cypress.platform === 'win32') { 37 | cy.exec('print cypress.json') 38 | .its('stderr').should('be.empty') 39 | } else { 40 | cy.exec('cat cypress.json') 41 | .its('stderr').should('be.empty') 42 | 43 | cy.exec('pwd') 44 | .its('code').should('eq', 0) 45 | } 46 | }) 47 | 48 | it('cy.focused() - get the DOM element that has focus', () => { 49 | // https://on.cypress.io/focused 50 | cy.get('.misc-form').find('#name').click() 51 | cy.focused().should('have.id', 'name') 52 | 53 | cy.get('.misc-form').find('#description').click() 54 | cy.focused().should('have.id', 'description') 55 | }) 56 | 57 | context('Cypress.Screenshot', function () { 58 | it('cy.screenshot() - take a screenshot', () => { 59 | // https://on.cypress.io/screenshot 60 | cy.screenshot('my-image') 61 | }) 62 | 63 | it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { 64 | Cypress.Screenshot.defaults({ 65 | blackout: ['.foo'], 66 | capture: 'viewport', 67 | clip: { x: 0, y: 0, width: 200, height: 200 }, 68 | scale: false, 69 | disableTimersAndAnimations: true, 70 | screenshotOnRunFailure: true, 71 | beforeScreenshot () { }, 72 | afterScreenshot () { }, 73 | }) 74 | }) 75 | }) 76 | 77 | it('cy.wrap() - wrap an object', () => { 78 | // https://on.cypress.io/wrap 79 | cy.wrap({ foo: 'bar' }) 80 | .should('have.property', 'foo') 81 | .and('include', 'bar') 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /cypress/integration/.examples/navigation.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Navigation', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io') 6 | cy.get('.navbar-nav').contains('Commands').click() 7 | cy.get('.dropdown-menu').contains('Navigation').click() 8 | }) 9 | 10 | it('cy.go() - go back or forward in the browser\'s history', () => { 11 | // https://on.cypress.io/go 12 | 13 | cy.location('pathname').should('include', 'navigation') 14 | 15 | cy.go('back') 16 | cy.location('pathname').should('not.include', 'navigation') 17 | 18 | cy.go('forward') 19 | cy.location('pathname').should('include', 'navigation') 20 | 21 | // clicking back 22 | cy.go(-1) 23 | cy.location('pathname').should('not.include', 'navigation') 24 | 25 | // clicking forward 26 | cy.go(1) 27 | cy.location('pathname').should('include', 'navigation') 28 | }) 29 | 30 | it('cy.reload() - reload the page', () => { 31 | // https://on.cypress.io/reload 32 | cy.reload() 33 | 34 | // reload the page without using the cache 35 | cy.reload(true) 36 | }) 37 | 38 | it('cy.visit() - visit a remote url', () => { 39 | // https://on.cypress.io/visit 40 | 41 | // Visit any sub-domain of your current domain 42 | 43 | // Pass options to the visit 44 | cy.visit('https://example.cypress.io/commands/navigation', { 45 | timeout: 50000, // increase total time for the visit to resolve 46 | onBeforeLoad (contentWindow) { 47 | // contentWindow is the remote page's window object 48 | expect(typeof contentWindow === 'object').to.be.true 49 | }, 50 | onLoad (contentWindow) { 51 | // contentWindow is the remote page's window object 52 | expect(typeof contentWindow === 'object').to.be.true 53 | }, 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /cypress/integration/.examples/network_requests.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Network Requests', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/network-requests') 6 | }) 7 | 8 | // Manage AJAX / XHR requests in your app 9 | 10 | it('cy.server() - control behavior of network requests and responses', () => { 11 | // https://on.cypress.io/server 12 | 13 | cy.server().should((server) => { 14 | // the default options on server 15 | // you can override any of these options 16 | expect(server.delay).to.eq(0) 17 | expect(server.method).to.eq('GET') 18 | expect(server.status).to.eq(200) 19 | expect(server.headers).to.be.null 20 | expect(server.response).to.be.null 21 | expect(server.onRequest).to.be.undefined 22 | expect(server.onResponse).to.be.undefined 23 | expect(server.onAbort).to.be.undefined 24 | 25 | // These options control the server behavior 26 | // affecting all requests 27 | 28 | // pass false to disable existing route stubs 29 | expect(server.enable).to.be.true 30 | // forces requests that don't match your routes to 404 31 | expect(server.force404).to.be.false 32 | // whitelists requests from ever being logged or stubbed 33 | expect(server.whitelist).to.be.a('function') 34 | }) 35 | 36 | cy.server({ 37 | method: 'POST', 38 | delay: 1000, 39 | status: 422, 40 | response: {}, 41 | }) 42 | 43 | // any route commands will now inherit the above options 44 | // from the server. anything we pass specifically 45 | // to route will override the defaults though. 46 | }) 47 | 48 | it('cy.request() - make an XHR request', () => { 49 | // https://on.cypress.io/request 50 | cy.request('https://jsonplaceholder.cypress.io/comments') 51 | .should((response) => { 52 | expect(response.status).to.eq(200) 53 | expect(response.body).to.have.length(500) 54 | expect(response).to.have.property('headers') 55 | expect(response).to.have.property('duration') 56 | }) 57 | }) 58 | 59 | 60 | it('cy.request() - verify response using BDD syntax', () => { 61 | cy.request('https://jsonplaceholder.cypress.io/comments') 62 | .then((response) => { 63 | // https://on.cypress.io/assertions 64 | expect(response).property('status').to.equal(200) 65 | expect(response).property('body').to.have.length(500) 66 | expect(response).to.include.keys('headers', 'duration') 67 | }) 68 | }) 69 | 70 | it('cy.request() with query parameters', () => { 71 | // will execute request 72 | // https://jsonplaceholder.cypress.io/comments?postId=1&id=3 73 | cy.request({ 74 | url: 'https://jsonplaceholder.cypress.io/comments', 75 | qs: { 76 | postId: 1, 77 | id: 3, 78 | }, 79 | }) 80 | .its('body') 81 | .should('be.an', 'array') 82 | .and('have.length', 1) 83 | .its('0') // yields first element of the array 84 | .should('contain', { 85 | postId: 1, 86 | id: 3, 87 | }) 88 | }) 89 | 90 | it('cy.request() - pass result to the second request', () => { 91 | // first, let's find out the userId of the first user we have 92 | cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') 93 | .its('body.0') // yields the first element of the returned list 94 | .then((user) => { 95 | expect(user).property('id').to.be.a('number') 96 | // make a new post on behalf of the user 97 | cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { 98 | userId: user.id, 99 | title: 'Cypress Test Runner', 100 | body: 'Fast, easy and reliable testing for anything that runs in a browser.', 101 | }) 102 | }) 103 | // note that the value here is the returned value of the 2nd request 104 | // which is the new post object 105 | .then((response) => { 106 | expect(response).property('status').to.equal(201) // new entity created 107 | expect(response).property('body').to.contain({ 108 | id: 101, // there are already 100 posts, so new entity gets id 101 109 | title: 'Cypress Test Runner', 110 | }) 111 | // we don't know the user id here - since it was in above closure 112 | // so in this test just confirm that the property is there 113 | expect(response.body).property('userId').to.be.a('number') 114 | }) 115 | }) 116 | 117 | it('cy.request() - save response in the shared test context', () => { 118 | // https://on.cypress.io/variables-and-aliases 119 | cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') 120 | .its('body.0') // yields the first element of the returned list 121 | .as('user') // saves the object in the test context 122 | .then(function () { 123 | // NOTE 👀 124 | // By the time this callback runs the "as('user')" command 125 | // has saved the user object in the test context. 126 | // To access the test context we need to use 127 | // the "function () { ... }" callback form, 128 | // otherwise "this" points at a wrong or undefined object! 129 | cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { 130 | userId: this.user.id, 131 | title: 'Cypress Test Runner', 132 | body: 'Fast, easy and reliable testing for anything that runs in a browser.', 133 | }) 134 | .its('body').as('post') // save the new post from the response 135 | }) 136 | .then(function () { 137 | // When this callback runs, both "cy.request" API commands have finished 138 | // and the test context has "user" and "post" objects set. 139 | // Let's verify them. 140 | expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id) 141 | }) 142 | }) 143 | 144 | it('cy.route() - route responses to matching requests', () => { 145 | // https://on.cypress.io/route 146 | 147 | let message = 'whoa, this comment does not exist' 148 | 149 | cy.server() 150 | 151 | // Listen to GET to comments/1 152 | cy.route('GET', 'comments/*').as('getComment') 153 | 154 | // we have code that gets a comment when 155 | // the button is clicked in scripts.js 156 | cy.get('.network-btn').click() 157 | 158 | // https://on.cypress.io/wait 159 | cy.wait('@getComment').its('status').should('eq', 200) 160 | 161 | // Listen to POST to comments 162 | cy.route('POST', '/comments').as('postComment') 163 | 164 | // we have code that posts a comment when 165 | // the button is clicked in scripts.js 166 | cy.get('.network-post').click() 167 | cy.wait('@postComment') 168 | 169 | // get the route 170 | cy.get('@postComment').should((xhr) => { 171 | expect(xhr.requestBody).to.include('email') 172 | expect(xhr.requestHeaders).to.have.property('Content-Type') 173 | expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()') 174 | }) 175 | 176 | // Stub a response to PUT comments/ **** 177 | cy.route({ 178 | method: 'PUT', 179 | url: 'comments/*', 180 | status: 404, 181 | response: { error: message }, 182 | delay: 500, 183 | }).as('putComment') 184 | 185 | // we have code that puts a comment when 186 | // the button is clicked in scripts.js 187 | cy.get('.network-put').click() 188 | 189 | cy.wait('@putComment') 190 | 191 | // our 404 statusCode logic in scripts.js executed 192 | cy.get('.network-put-comment').should('contain', message) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /cypress/integration/.examples/querying.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Querying', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/querying') 6 | }) 7 | 8 | // The most commonly used query is 'cy.get()', you can 9 | // think of this like the '$' in jQuery 10 | 11 | it('cy.get() - query DOM elements', () => { 12 | // https://on.cypress.io/get 13 | 14 | cy.get('#query-btn').should('contain', 'Button') 15 | 16 | cy.get('.query-btn').should('contain', 'Button') 17 | 18 | cy.get('#querying .well>button:first').should('contain', 'Button') 19 | // ↲ 20 | // Use CSS selectors just like jQuery 21 | 22 | cy.get('[data-test-id="test-example"]').should('have.class', 'example') 23 | 24 | // 'cy.get()' yields jQuery object, you can get its attribute 25 | // by invoking `.attr()` method 26 | cy.get('[data-test-id="test-example"]') 27 | .invoke('attr', 'data-test-id') 28 | .should('equal', 'test-example') 29 | 30 | // or you can get element's CSS property 31 | cy.get('[data-test-id="test-example"]') 32 | .invoke('css', 'position') 33 | .should('equal', 'static') 34 | 35 | // or use assertions directly during 'cy.get()' 36 | // https://on.cypress.io/assertions 37 | cy.get('[data-test-id="test-example"]') 38 | .should('have.attr', 'data-test-id', 'test-example') 39 | .and('have.css', 'position', 'static') 40 | }) 41 | 42 | it('cy.contains() - query DOM elements with matching content', () => { 43 | // https://on.cypress.io/contains 44 | cy.get('.query-list') 45 | .contains('bananas') 46 | .should('have.class', 'third') 47 | 48 | // we can pass a regexp to `.contains()` 49 | cy.get('.query-list') 50 | .contains(/^b\w+/) 51 | .should('have.class', 'third') 52 | 53 | cy.get('.query-list') 54 | .contains('apples') 55 | .should('have.class', 'first') 56 | 57 | // passing a selector to contains will 58 | // yield the selector containing the text 59 | cy.get('#querying') 60 | .contains('ul', 'oranges') 61 | .should('have.class', 'query-list') 62 | 63 | cy.get('.query-button') 64 | .contains('Save Form') 65 | .should('have.class', 'btn') 66 | }) 67 | 68 | it('.within() - query DOM elements within a specific element', () => { 69 | // https://on.cypress.io/within 70 | cy.get('.query-form').within(() => { 71 | cy.get('input:first').should('have.attr', 'placeholder', 'Email') 72 | cy.get('input:last').should('have.attr', 'placeholder', 'Password') 73 | }) 74 | }) 75 | 76 | it('cy.root() - query the root DOM element', () => { 77 | // https://on.cypress.io/root 78 | 79 | // By default, root is the document 80 | cy.root().should('match', 'html') 81 | 82 | cy.get('.query-ul').within(() => { 83 | // In this within, the root is now the ul DOM element 84 | cy.root().should('have.class', 'query-ul') 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /cypress/integration/.examples/spies_stubs_clocks.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Spies, Stubs, and Clock', () => { 4 | it('cy.spy() - wrap a method in a spy', () => { 5 | // https://on.cypress.io/spy 6 | cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') 7 | 8 | const obj = { 9 | foo () {}, 10 | } 11 | 12 | const spy = cy.spy(obj, 'foo').as('anyArgs') 13 | 14 | obj.foo() 15 | 16 | expect(spy).to.be.called 17 | }) 18 | 19 | it('cy.spy() retries until assertions pass', () => { 20 | cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') 21 | 22 | const obj = { 23 | /** 24 | * Prints the argument passed 25 | * @param x {any} 26 | */ 27 | foo (x) { 28 | console.log('obj.foo called with', x) 29 | }, 30 | } 31 | 32 | cy.spy(obj, 'foo').as('foo') 33 | 34 | setTimeout(() => { 35 | obj.foo('first') 36 | }, 500) 37 | 38 | setTimeout(() => { 39 | obj.foo('second') 40 | }, 2500) 41 | 42 | cy.get('@foo').should('have.been.calledTwice') 43 | }) 44 | 45 | it('cy.stub() - create a stub and/or replace a function with stub', () => { 46 | // https://on.cypress.io/stub 47 | cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') 48 | 49 | const obj = { 50 | /** 51 | * prints both arguments to the console 52 | * @param a {string} 53 | * @param b {string} 54 | */ 55 | foo (a, b) { 56 | console.log('a', a, 'b', b) 57 | }, 58 | } 59 | 60 | const stub = cy.stub(obj, 'foo').as('foo') 61 | 62 | obj.foo('foo', 'bar') 63 | 64 | expect(stub).to.be.called 65 | }) 66 | 67 | it('cy.clock() - control time in the browser', () => { 68 | // https://on.cypress.io/clock 69 | 70 | // create the date in UTC so its always the same 71 | // no matter what local timezone the browser is running in 72 | const now = new Date(Date.UTC(2017, 2, 14)).getTime() 73 | 74 | cy.clock(now) 75 | cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') 76 | cy.get('#clock-div').click() 77 | .should('have.text', '1489449600') 78 | }) 79 | 80 | it('cy.tick() - move time in the browser', () => { 81 | // https://on.cypress.io/tick 82 | 83 | // create the date in UTC so its always the same 84 | // no matter what local timezone the browser is running in 85 | const now = new Date(Date.UTC(2017, 2, 14)).getTime() 86 | 87 | cy.clock(now) 88 | cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') 89 | cy.get('#tick-div').click() 90 | .should('have.text', '1489449600') 91 | cy.tick(10000) // 10 seconds passed 92 | cy.get('#tick-div').click() 93 | .should('have.text', '1489449610') 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /cypress/integration/.examples/traversal.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Traversal', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/traversal') 6 | }) 7 | 8 | it('.children() - get child DOM elements', () => { 9 | // https://on.cypress.io/children 10 | cy.get('.traversal-breadcrumb') 11 | .children('.active') 12 | .should('contain', 'Data') 13 | }) 14 | 15 | it('.closest() - get closest ancestor DOM element', () => { 16 | // https://on.cypress.io/closest 17 | cy.get('.traversal-badge') 18 | .closest('ul') 19 | .should('have.class', 'list-group') 20 | }) 21 | 22 | it('.eq() - get a DOM element at a specific index', () => { 23 | // https://on.cypress.io/eq 24 | cy.get('.traversal-list>li') 25 | .eq(1).should('contain', 'siamese') 26 | }) 27 | 28 | it('.filter() - get DOM elements that match the selector', () => { 29 | // https://on.cypress.io/filter 30 | cy.get('.traversal-nav>li') 31 | .filter('.active').should('contain', 'About') 32 | }) 33 | 34 | it('.find() - get descendant DOM elements of the selector', () => { 35 | // https://on.cypress.io/find 36 | cy.get('.traversal-pagination') 37 | .find('li').find('a') 38 | .should('have.length', 7) 39 | }) 40 | 41 | it('.first() - get first DOM element', () => { 42 | // https://on.cypress.io/first 43 | cy.get('.traversal-table td') 44 | .first().should('contain', '1') 45 | }) 46 | 47 | it('.last() - get last DOM element', () => { 48 | // https://on.cypress.io/last 49 | cy.get('.traversal-buttons .btn') 50 | .last().should('contain', 'Submit') 51 | }) 52 | 53 | it('.next() - get next sibling DOM element', () => { 54 | // https://on.cypress.io/next 55 | cy.get('.traversal-ul') 56 | .contains('apples').next().should('contain', 'oranges') 57 | }) 58 | 59 | it('.nextAll() - get all next sibling DOM elements', () => { 60 | // https://on.cypress.io/nextall 61 | cy.get('.traversal-next-all') 62 | .contains('oranges') 63 | .nextAll().should('have.length', 3) 64 | }) 65 | 66 | it('.nextUntil() - get next sibling DOM elements until next el', () => { 67 | // https://on.cypress.io/nextuntil 68 | cy.get('#veggies') 69 | .nextUntil('#nuts').should('have.length', 3) 70 | }) 71 | 72 | it('.not() - remove DOM elements from set of DOM elements', () => { 73 | // https://on.cypress.io/not 74 | cy.get('.traversal-disabled .btn') 75 | .not('[disabled]').should('not.contain', 'Disabled') 76 | }) 77 | 78 | it('.parent() - get parent DOM element from DOM elements', () => { 79 | // https://on.cypress.io/parent 80 | cy.get('.traversal-mark') 81 | .parent().should('contain', 'Morbi leo risus') 82 | }) 83 | 84 | it('.parents() - get parent DOM elements from DOM elements', () => { 85 | // https://on.cypress.io/parents 86 | cy.get('.traversal-cite') 87 | .parents().should('match', 'blockquote') 88 | }) 89 | 90 | it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { 91 | // https://on.cypress.io/parentsuntil 92 | cy.get('.clothes-nav') 93 | .find('.active') 94 | .parentsUntil('.clothes-nav') 95 | .should('have.length', 2) 96 | }) 97 | 98 | it('.prev() - get previous sibling DOM element', () => { 99 | // https://on.cypress.io/prev 100 | cy.get('.birds').find('.active') 101 | .prev().should('contain', 'Lorikeets') 102 | }) 103 | 104 | it('.prevAll() - get all previous sibling DOM elements', () => { 105 | // https://on.cypress.io/prevAll 106 | cy.get('.fruits-list').find('.third') 107 | .prevAll().should('have.length', 2) 108 | }) 109 | 110 | it('.prevUntil() - get all previous sibling DOM elements until el', () => { 111 | // https://on.cypress.io/prevUntil 112 | cy.get('.foods-list').find('#nuts') 113 | .prevUntil('#veggies').should('have.length', 3) 114 | }) 115 | 116 | it('.siblings() - get all sibling DOM elements', () => { 117 | // https://on.cypress.io/siblings 118 | cy.get('.traversal-pills .active') 119 | .siblings().should('have.length', 2) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /cypress/integration/.examples/utilities.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Utilities', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/utilities') 6 | }) 7 | 8 | it('Cypress._ - call a lodash method', () => { 9 | // https://on.cypress.io/_ 10 | cy.request('https://jsonplaceholder.cypress.io/users') 11 | .then((response) => { 12 | let ids = Cypress._.chain(response.body).map('id').take(3).value() 13 | 14 | expect(ids).to.deep.eq([1, 2, 3]) 15 | }) 16 | }) 17 | 18 | it('Cypress.$ - call a jQuery method', () => { 19 | // https://on.cypress.io/$ 20 | let $li = Cypress.$('.utility-jquery li:first') 21 | 22 | cy.wrap($li) 23 | .should('not.have.class', 'active') 24 | .click() 25 | .should('have.class', 'active') 26 | }) 27 | 28 | it('Cypress.Blob - blob utilities and base64 string conversion', () => { 29 | // https://on.cypress.io/blob 30 | cy.get('.utility-blob').then(($div) => 31 | // https://github.com/nolanlawson/blob-util#imgSrcToDataURL 32 | // get the dataUrl string for the javascript-logo 33 | Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') 34 | .then((dataUrl) => { 35 | // create an element and set its src to the dataUrl 36 | let img = Cypress.$('', { src: dataUrl }) 37 | 38 | // need to explicitly return cy here since we are initially returning 39 | // the Cypress.Blob.imgSrcToDataURL promise to our test 40 | // append the image 41 | $div.append(img) 42 | 43 | cy.get('.utility-blob img').click() 44 | .should('have.attr', 'src', dataUrl) 45 | })) 46 | }) 47 | 48 | it('Cypress.minimatch - test out glob patterns against strings', () => { 49 | // https://on.cypress.io/minimatch 50 | let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { 51 | matchBase: true, 52 | }) 53 | 54 | expect(matching, 'matching wildcard').to.be.true 55 | 56 | matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { 57 | matchBase: true, 58 | }) 59 | expect(matching, 'comments').to.be.false 60 | 61 | // ** matches against all downstream path segments 62 | matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { 63 | matchBase: true, 64 | }) 65 | expect(matching, 'comments').to.be.true 66 | 67 | // whereas * matches only the next path segment 68 | 69 | matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { 70 | matchBase: false, 71 | }) 72 | expect(matching, 'comments').to.be.false 73 | }) 74 | 75 | 76 | it('Cypress.moment() - format or parse dates using a moment method', () => { 77 | // https://on.cypress.io/moment 78 | const time = Cypress.moment().utc('2014-04-25T19:38:53.196Z').format('h:mm A') 79 | 80 | expect(time).to.be.a('string') 81 | 82 | cy.get('.utility-moment').contains('3:38 PM') 83 | .should('have.class', 'badge') 84 | 85 | // the time in the element should be between 3pm and 5pm 86 | const start = Cypress.moment('3:00 PM', 'LT') 87 | const end = Cypress.moment('5:00 PM', 'LT') 88 | 89 | cy.get('.utility-moment .badge') 90 | .should(($el) => { 91 | // parse American time like "3:38 PM" 92 | const m = Cypress.moment($el.text().trim(), 'LT') 93 | 94 | // display hours + minutes + AM|PM 95 | const f = 'h:mm A' 96 | 97 | expect(m.isBetween(start, end), 98 | `${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true 99 | }) 100 | }) 101 | 102 | 103 | it('Cypress.Promise - instantiate a bluebird promise', () => { 104 | // https://on.cypress.io/promise 105 | let waited = false 106 | 107 | /** 108 | * @return Bluebird 109 | */ 110 | function waitOneSecond () { 111 | // return a promise that resolves after 1 second 112 | // @ts-ignore TS2351 (new Cypress.Promise) 113 | return new Cypress.Promise((resolve, reject) => { 114 | setTimeout(() => { 115 | // set waited to true 116 | waited = true 117 | 118 | // resolve with 'foo' string 119 | resolve('foo') 120 | }, 1000) 121 | }) 122 | } 123 | 124 | cy.then(() => 125 | // return a promise to cy.then() that 126 | // is awaited until it resolves 127 | // @ts-ignore TS7006 128 | waitOneSecond().then((str) => { 129 | expect(str).to.eq('foo') 130 | expect(waited).to.be.true 131 | })) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /cypress/integration/.examples/viewport.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Viewport', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/viewport') 6 | }) 7 | 8 | it('cy.viewport() - set the viewport size and dimension', () => { 9 | // https://on.cypress.io/viewport 10 | 11 | cy.get('#navbar').should('be.visible') 12 | cy.viewport(320, 480) 13 | 14 | // the navbar should have collapse since our screen is smaller 15 | cy.get('#navbar').should('not.be.visible') 16 | cy.get('.navbar-toggle').should('be.visible').click() 17 | cy.get('.nav').find('a').should('be.visible') 18 | 19 | // lets see what our app looks like on a super large screen 20 | cy.viewport(2999, 2999) 21 | 22 | // cy.viewport() accepts a set of preset sizes 23 | // to easily set the screen to a device's width and height 24 | 25 | // We added a cy.wait() between each viewport change so you can see 26 | // the change otherwise it is a little too fast to see :) 27 | 28 | cy.viewport('macbook-15') 29 | cy.wait(200) 30 | cy.viewport('macbook-13') 31 | cy.wait(200) 32 | cy.viewport('macbook-11') 33 | cy.wait(200) 34 | cy.viewport('ipad-2') 35 | cy.wait(200) 36 | cy.viewport('ipad-mini') 37 | cy.wait(200) 38 | cy.viewport('iphone-6+') 39 | cy.wait(200) 40 | cy.viewport('iphone-6') 41 | cy.wait(200) 42 | cy.viewport('iphone-5') 43 | cy.wait(200) 44 | cy.viewport('iphone-4') 45 | cy.wait(200) 46 | cy.viewport('iphone-3') 47 | cy.wait(200) 48 | 49 | // cy.viewport() accepts an orientation for all presets 50 | // the default orientation is 'portrait' 51 | cy.viewport('ipad-2', 'portrait') 52 | cy.wait(200) 53 | cy.viewport('iphone-4', 'landscape') 54 | cy.wait(200) 55 | 56 | // The viewport will be reset back to the default dimensions 57 | // in between tests (the default can be set in cypress.json) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /cypress/integration/.examples/waiting.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Waiting', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/waiting') 6 | }) 7 | // BE CAREFUL of adding unnecessary wait times. 8 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 9 | 10 | // https://on.cypress.io/wait 11 | it('cy.wait() - wait for a specific amount of time', () => { 12 | cy.get('.wait-input1').type('Wait 1000ms after typing') 13 | cy.wait(1000) 14 | cy.get('.wait-input2').type('Wait 1000ms after typing') 15 | cy.wait(1000) 16 | cy.get('.wait-input3').type('Wait 1000ms after typing') 17 | cy.wait(1000) 18 | }) 19 | 20 | it('cy.wait() - wait for a specific route', () => { 21 | cy.server() 22 | 23 | // Listen to GET to comments/1 24 | cy.route('GET', 'comments/*').as('getComment') 25 | 26 | // we have code that gets a comment when 27 | // the button is clicked in scripts.js 28 | cy.get('.network-btn').click() 29 | 30 | // wait for GET comments/1 31 | cy.wait('@getComment').its('status').should('eq', 200) 32 | }) 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /cypress/integration/.examples/window.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Window', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/window') 6 | }) 7 | 8 | it('cy.window() - get the global window object', () => { 9 | // https://on.cypress.io/window 10 | cy.window().should('have.property', 'top') 11 | }) 12 | 13 | it('cy.document() - get the document object', () => { 14 | // https://on.cypress.io/document 15 | cy.document().should('have.property', 'charset').and('eq', 'UTF-8') 16 | }) 17 | 18 | it('cy.title() - get the title', () => { 19 | // https://on.cypress.io/title 20 | cy.title().should('include', 'Kitchen Sink') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/integration/authentication.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Authentication', () => { 4 | 5 | it('should only be able to get to pages where allowUnauthenticated=true', () => { 6 | cy.visit('localhost:8080/#/') 7 | 8 | cy.get("#logout") 9 | .click() 10 | 11 | cy.visit("localhost:8080/#/test-jig") 12 | 13 | cy.get("#login") 14 | 15 | }) 16 | 17 | // eslint-disable-next-line no-undef 18 | afterEach(() => { 19 | cy.get("#logout") 20 | .click() 21 | }) 22 | 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/integration/plan.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /// 3 | 4 | context('Plan', () => { 5 | beforeEach(() => { 6 | cy.clearLocalStorage().should((ls) => { 7 | expect(ls.getItem('teamID')).to.be.null 8 | expect(ls.getItem('startOn')).to.be.null 9 | expect(ls.getItem('slidesToDisplay')).to.be.null 10 | }) 11 | 12 | cy.visit('localhost:8080/#/login?origin=/plan') 13 | .get("#login") 14 | .click() 15 | }) 16 | 17 | it('drags and drops stuff around', () => { 18 | cy.url().should('include', 'teamID=team1') 19 | cy.url().should('include', 'startOn=0') 20 | cy.url().should('include', 'slidesToDisplay=1') 21 | 22 | cy.should(() => { 23 | expect(localStorage.getItem('/plan.startOn')).to.eq('0') 24 | expect(localStorage.getItem('/plan.slidesToDisplay')).to.eq('1') 25 | }) 26 | 27 | cy.get('#practice5') 28 | .contains("Network Originated Scans") 29 | .trigger('dragstart') 30 | 31 | cy.get('#pan-right') 32 | .contains("Doing") 33 | .trigger('dragenter') 34 | 35 | cy.get('#pan-right') 36 | .should('have.class', 'has-background-grey-lighter') 37 | 38 | cy.get('#pan-right') 39 | .trigger('dragleave') 40 | 41 | cy.get('#pan-right') 42 | .should('not.have.class', 'has-background-grey-lighter') 43 | 44 | cy.get('#pan-right') 45 | .trigger('dragenter') 46 | .trigger('drop') 47 | 48 | cy.get('#pan-right') 49 | .should('not.have.class', 'has-background-grey-lighter') 50 | .click() 51 | 52 | cy.should(() => { 53 | expect(localStorage.getItem('/plan.startOn')).to.eq('1') 54 | expect(localStorage.getItem('/plan.slidesToDisplay')).to.eq('1') 55 | }) 56 | 57 | cy.url().should('include', 'startOn=1') 58 | 59 | cy.get('#pan-left') 60 | .contains("Todo") 61 | 62 | cy.get('#practice5') 63 | .contains("Network Originated Scans") 64 | 65 | cy.visit('localhost:8080/#/test-jig') 66 | cy.wait(4000) 67 | cy.url().should('include', 'teamID=team1') 68 | cy.url().should('not.include', 'startOn') 69 | cy.url().should('not.include', 'slidesToDisplay') 70 | 71 | cy.visit('localhost:8080/#/plan') 72 | cy.wait(4000) 73 | cy.url().should('include', 'teamID=team1') 74 | cy.url().should('include', 'startOn=1') 75 | cy.url().should('include', 'slidesToDisplay=1') 76 | 77 | 78 | cy.get('#practice5.card') 79 | .contains("Network Originated Scans") 80 | .trigger('dragstart') 81 | 82 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 83 | .trigger('dragenter') 84 | 85 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 86 | .should('have.class', 'has-background-grey-lighter') 87 | 88 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 89 | .trigger('dragleave') 90 | 91 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 92 | .should('not.have.class', 'has-background-grey-lighter') 93 | 94 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 95 | .trigger('dragenter') 96 | .trigger('drop') 97 | 98 | cy.get(`#${CSS.escape('(queue1, Actions)')}`) 99 | .contains("Network Originated Scans") 100 | 101 | }) 102 | 103 | // eslint-disable-next-line no-undef 104 | afterEach(() => { 105 | cy.get("#logout") 106 | .click() 107 | }) 108 | 109 | }) 110 | -------------------------------------------------------------------------------- /cypress/integration/realtime.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Realtime', () => { 4 | beforeEach(() => { 5 | cy.visit('localhost:8080/#/login?origin=/test-jig') 6 | .get("#login") 7 | .click() 8 | }) 9 | 10 | it('keeps stores synchronized', () => { 11 | cy.get('#a-value') 12 | .contains("2000") 13 | 14 | cy.get('#a-button') 15 | .click() 16 | 17 | cy.get('#a-value') 18 | .contains("2001") 19 | 20 | cy.get('#a-prime-value') 21 | .contains("2001") 22 | 23 | cy.get('#a-prime-button') 24 | .click() 25 | 26 | cy.get('#a-value') 27 | .contains("2002") 28 | 29 | cy.get('#a-prime-value') 30 | .contains("2001") // Because we have ignoreLocalSet=true 31 | }) 32 | 33 | it('logs out and back in', () => { 34 | cy.get("#logout") 35 | .click() 36 | 37 | cy.get("#login") 38 | .click() 39 | 40 | cy.get('#a-button') 41 | .click() 42 | }) 43 | 44 | // eslint-disable-next-line no-undef 45 | afterEach(() => { 46 | cy.get("#logout") 47 | .click() 48 | }) 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /cypress/integration/window.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Window', () => { 4 | beforeEach(() => { 5 | cy.visit('localhost:8080/') 6 | }) 7 | 8 | it('cy.window() - get the global window object', () => { 9 | cy.window().should('have.property', 'top') 10 | }) 11 | 12 | it('cy.document() - get the document object', () => { 13 | cy.document().should('have.property', 'charset').and('eq', 'UTF-8') 14 | }) 15 | 16 | it('cy.title() - get the title', () => { 17 | cy.title().should('include', 'MatrX') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | on('task', require('@cypress/code-coverage/task')) 18 | } 19 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | import '@cypress/code-coverage/support' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /dist/MatrXCloseWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transformation-dev/matrx/85087a4e90c127d43f885bf8083ef19fe27bad40/dist/MatrXCloseWhite.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/matrx-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transformation-dev/matrx/85087a4e90c127d43f885bf8083ef19fe27bad40/dist/matrx-favicon.png -------------------------------------------------------------------------------- /dist/matrxlogowhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transformation-dev/matrx/85087a4e90c127d43f885bf8083ef19fe27bad40/dist/matrxlogowhite.png -------------------------------------------------------------------------------- /dist/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | {"route": "/api/context", "allowedRoles": ["authenticated"]} 4 | ], 5 | "defaultHeaders": { 6 | "content-security-policy": "script-src 'self'; object-src 'none'; base-uri 'self'" 7 | } 8 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | ".", 4 | "packages/*" 5 | ], 6 | "version": "0.6.0" 7 | } 8 | -------------------------------------------------------------------------------- /matrx.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrx", 3 | "version": "0.6.0", 4 | "description": "MatrX main application", 5 | "main": "src/server.js", 6 | "svelte": "src/App.svelte", 7 | "private": true, 8 | "nodemonConfig": { 9 | "verbose": true, 10 | "delay": "1000", 11 | "watch": [ 12 | "packages", 13 | "dist", 14 | "src/server.js" 15 | ] 16 | }, 17 | "scripts": { 18 | "build": "webpack", 19 | "dev": "webpack -w", 20 | "start": "node src/server", 21 | "eslint": "npx eslint -c .eslintrc.js --ext .js,.svelte,.html .", 22 | "lint": "npm run eslint", 23 | "cypress": "cypress run", 24 | "cypress:active": "cypress run --spec cypress/integration/authentication.spec.js", 25 | "test": "start-server-and-test start http://localhost:8080 cypress", 26 | "test:active": "start-server-and-test start http://localhost:8080 cypress:active", 27 | "ports": "sudo netstat -ltnp", 28 | "coverage:build": "webpack --config webpack.config.coverage-client.js", 29 | "coverage:report": "nyc report --reporter=text-summary", 30 | "coverage:check": "nyc check-coverage --statements 80 --branches 60 --functions 80 --lines 85", 31 | "coverage": "npm run coverage:build; nyc --reporter=lcov npm test" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/matrx-transformation/matrx.git" 36 | }, 37 | "keywords": [ 38 | "cybersecurity", 39 | "DevSecOps", 40 | "svelte", 41 | "svelte3" 42 | ], 43 | "author": "The MatrX team", 44 | "license": "UNLICENSED", 45 | "dependencies": { 46 | "@babel/polyfill": "^7.10.4", 47 | "@matrx/svelte-realtime-adapter-cosmos-db-temporal": "file:packages/svelte-realtime-adapter-cosmos-db-temporal", 48 | "@matrx/svelte-realtime-server": "file:packages/svelte-realtime-server", 49 | "bulma-badge": "^3.0.1", 50 | "compression": "^1.7.1", 51 | "cookie": "^0.4.1", 52 | "cookie-parser": "^1.4.5", 53 | "csurf": "^1.11.0", 54 | "debug": "^4.1.1", 55 | "express": "^4.17.1", 56 | "express-session": "^1.17.1", 57 | "helmet": "^3.23.3", 58 | "lodash": "^4.17.15", 59 | "passport": "^0.4.1", 60 | "passport-local": "^1.0.0", 61 | "regexparam": "^1.3.0", 62 | "serve-static": "^1.14.1", 63 | "session-file-store": "^1.4.0", 64 | "short-unique-id": "^1.1.1", 65 | "svelte-awesome": "^2.2.1", 66 | "whatwg-fetch": "^3.0.0" 67 | }, 68 | "devDependencies": { 69 | "@babel/cli": "^7.10.5", 70 | "@babel/core": "^7.10.5", 71 | "@babel/preset-env": "^7.10.4", 72 | "@creativebulma/bulma-tooltip": "^1.2.0", 73 | "@cypress/code-coverage": "^1.14.0", 74 | "@matrx/dragster": "file:packages/dragster", 75 | "@matrx/svelte-realtime-store": "file:packages/svelte-realtime-store", 76 | "@matrx/svelte-viewstate-store": "file:packages/svelte-viewstate-store", 77 | "@typescript-eslint/eslint-plugin": "^3.6.1", 78 | "@typescript-eslint/parser": "^3.6.1", 79 | "babel-eslint": "^10.1.0", 80 | "babel-loader": "^8.1.0", 81 | "browserify": "^16.5.1", 82 | "bulma": "^0.9.0", 83 | "bulmaswatch": "^0.8.1", 84 | "coffeescript": "^2.5.1", 85 | "css-loader": "^3.6.0", 86 | "cypress": "^4.9.0", 87 | "dragster": "^0.1.3", 88 | "eslint": "^7.5.0", 89 | "eslint-plugin-babel": "^5.3.1", 90 | "eslint-plugin-html": "^6.0.2", 91 | "eslint-plugin-svelte3": "^2.7.3", 92 | "extract-text-webpack-plugin": "^3.0.2", 93 | "istanbul-instrumenter-loader": "^3.0.1", 94 | "lerna": "^3.22.1", 95 | "mini-css-extract-plugin": "^0.8.2", 96 | "node-sass": "^4.14.1", 97 | "nodemon": "^1.19.4", 98 | "nyc": "^14.1.1", 99 | "pusher-js": "^6.0.3", 100 | "sass-loader": "^9.0.2", 101 | "standardx": "^5.0.0", 102 | "start-server-and-test": "^1.11.0", 103 | "style-loader": "^1.2.1", 104 | "svelte": "^3.23.2", 105 | "svelte-loader": "^2.13.6", 106 | "svelte-preprocess": "^4.0.8", 107 | "svelte-spa-router": "^2.2.0", 108 | "tap-spec": "^5.0.0", 109 | "typescript": "^3.9.7", 110 | "webpack": "^4.43.0", 111 | "webpack-cli": "^3.3.12" 112 | }, 113 | "peerDependencies": { 114 | "svelte": "^3.5.0" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/dragster/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Ben Smithett 2 | 3 | Copyright (c) 2020 Larry Maccherone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/dragster/README.md: -------------------------------------------------------------------------------- 1 | # `@matrx/dragster` 2 | 3 | `@matrx/dragster` is a port of [Ben Smithett's dragster](http://bensmithett.github.io/dragster) to ES6. 4 | It wraps the native dragenter/dragleave to behave like mouse event when hovering over child DOM elements. 5 | 6 | ## Differences from the original 7 | 8 | * Has been converted from CoffeeScript to JavaScript 9 | * Has had its events renamed to 'dragster-enter' and 10 | 'dragster-leave' 11 | * Uses ES6 Class 12 | * Keeps track of its instances for later reference so your drop 13 | callback has a way to call `reset()` 14 | * Provides a `destroy()` method that can be used as a callback to 15 | remove said instances. If you use this with Svelte's `use:` 16 | directive, then Svelte will automatically call `destroy()` as 17 | something is removed from the DOM. 18 | 19 | ## Usage 20 | 21 | To install with npm 22 | 23 | ```bash 24 | npm install --save @matrx/dragster 25 | ``` 26 | 27 | In a .svelte file 28 | 29 | ```html 30 | 67 | 68 | 83 | 84 |
85 | Drop something on me 86 |
87 | 88 |
89 | Drag me 90 |
91 | ``` 92 | 93 | React's JSX and I suspect Angular, Vue, etc. have a similar syntax to above. In JSX, it's `onDragster-start`. You may also have to 94 | create Dragster instances yourself if your UI tech doesn't have 95 | the equivalent to Svelte's `use:` and you should probably manually 96 | call `destroy()`. 97 | 98 | Plain HTML/JavaScript is essentially the same except you'll specify 99 | the event listeners with `addEventListener()` like in the 100 | [MDN web docs example](https://developer.mozilla.org/en-US/docs/Web/API/Document/drag_event) except that you have to instantiate the 101 | Dragster instances yourself and you should probably manually call 102 | `destroy()`. 103 | -------------------------------------------------------------------------------- /packages/dragster/__tests__/dragster.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Dragster} = require('../dragster'); 4 | 5 | describe('@matrx/dragster', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dragster/dragster.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('matrx:dragster') 2 | 3 | export class Dragster { 4 | constructor(el) { 5 | this.dragenter = this.dragenter.bind(this) 6 | this.dragleave = this.dragleave.bind(this) 7 | this.el = el 8 | this.first = false 9 | this.second = false 10 | this.el.addEventListener("dragenter", this.dragenter, false) 11 | this.el.addEventListener("dragleave", this.dragleave, false) 12 | this.destroy = this._destroy.bind(this) 13 | if (!Dragster.dragsters) { 14 | Dragster.dragsters = {} 15 | } 16 | if (Dragster.dragsters[this.el.id]) { 17 | debug('WARNING: A Dragster for element with id "%O" has already been instantiated. This replaces that.', this.el.id) 18 | } 19 | Dragster.dragsters[this.el.id] = this 20 | } 21 | 22 | static getDragster(id) { 23 | return Dragster.dragsters[id] 24 | } 25 | 26 | static reset(el) { 27 | Dragster.dragsters[el.id].reset() 28 | } 29 | 30 | dragenter(event) { 31 | event.preventDefault() 32 | if (this.first) { 33 | this.second = true 34 | } else { 35 | this.first = true 36 | this.customEvent = document.createEvent("CustomEvent") 37 | this.customEvent.initCustomEvent("dragster-enter", true, true, { 38 | dataTransfer: event.dataTransfer, 39 | sourceEvent: event 40 | }) 41 | this.el.dispatchEvent(this.customEvent) 42 | } 43 | } 44 | 45 | dragleave(event) { 46 | if (this.second) { 47 | this.second = false 48 | } else if (this.first) { 49 | this.first = false 50 | } 51 | if (!this.first && !this.second) { 52 | this.customEvent = document.createEvent("CustomEvent") 53 | this.customEvent.initCustomEvent("dragster-leave", true, true, { 54 | dataTransfer: event.dataTransfer, 55 | sourceEvent: event 56 | }) 57 | this.el.dispatchEvent(this.customEvent) 58 | } 59 | } 60 | 61 | removeListeners() { 62 | this.el.removeEventListener("dragenter", this.dragenter, false) 63 | return this.el.removeEventListener("dragleave", this.dragleave, false) 64 | } 65 | 66 | // Must call after drop or a second drop to the same target sometimes gets missed 67 | reset() { 68 | this.first = false 69 | return this.second = false 70 | } 71 | 72 | _destroy() { 73 | if (Dragster.dragsters[this.el.id]) { 74 | Dragster.dragsters[this.el.id].removeListeners() 75 | delete Dragster.dragsters[this.el.id] 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/dragster/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/dragster", 3 | "version": "0.6.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /packages/dragster/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/dragster", 3 | "version": "0.6.0", 4 | "description": "Port of Ben Smithett's dragster to ES6. Wraps native dragenter/dragleave to behave like mouse event when hovering over child DOM elements.", 5 | "keywords": [ 6 | "drag-and-drop", 7 | "drag", 8 | "drop", 9 | "dragenter", 10 | "dragleave", 11 | "svelte", 12 | "svelte3" 13 | ], 14 | "author": "Larry Maccherone ", 15 | "homepage": "https://github.com/matrx-transformation/matrx/tree/master/packages/dragster#readme", 16 | "license": "MIT", 17 | "main": "dragster.js", 18 | "type": "module", 19 | "directories": { 20 | "test": "__tests__" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/matrx-transformation/matrx/tree/master/packages/dragster" 28 | }, 29 | "scripts": { 30 | "test": "echo \"Error: run tests from root\" && exit 1" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/matrx-transformation/matrx/issues" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Larry Maccherone 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/README.md: -------------------------------------------------------------------------------- 1 | # `@matrx/svelte-realtime-adapter-cosmos-db` 2 | 3 | svelte-realtime-adapter-cosmos-db is a database adapter for the [`svelte-realtime-coordinator`](https://www.npmjs.com/package/@matrx/svelte-realtime-coordinator). For a summary of how to use all of the svelte-realtime-... packages, go over to the [`svelte-realtime-store`](https://www.npmjs.com/package/@matrx/svelte-realtime-store). 4 | 5 | ## Usage 6 | 7 | To install with npm 8 | 9 | ```bash 10 | npm install --save @matrx/svelte-realtime-adapter-cosmos-db 11 | ``` 12 | 13 | Modify your server.js file to look something like this 14 | 15 | ```js 16 | import http from 'http' 17 | import sirv from 'sirv' 18 | import express from 'express' 19 | import compression from 'compression' 20 | import * as sapper from '@sapper/server' 21 | import uuidv4 from 'uuid/v4' 22 | 23 | // Look here 24 | import { getServer } from '@matrx/svelte-realtime-server' 25 | 26 | const { PORT, NODE_ENV } = process.env 27 | const dev = NODE_ENV === 'development' 28 | 29 | const app = express() 30 | const server = http.createServer(app) 31 | 32 | // And here 33 | const nsp = getServer(server) 34 | 35 | app.use( 36 | compression({ threshold: 0 }), 37 | sirv('static', { dev }), 38 | sapper.middleware() 39 | ) 40 | 41 | server.listen(PORT, err => { 42 | if (err) console.log('error', err); 43 | }) 44 | 45 | ``` -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "defaultCommandTimeout": 10000 4 | } 5 | -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-realtime-adapter-cosmos-db-temporal", 3 | "version": "0.6.0", 4 | "description": "Cosmos DB database adapter (temporal) for svelte-realtime-store/server", 5 | "keywords": [ 6 | "realtime", 7 | "real-time", 8 | "sapper", 9 | "svelte", 10 | "svelte3", 11 | "store", 12 | "socket.io", 13 | "websocket", 14 | "azure", 15 | "cosmosdb", 16 | "cosmos-db", 17 | "cosmos db" 18 | ], 19 | "author": "Larry Maccherone ", 20 | "homepage": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-adapter-cosmos-db#readme", 21 | "license": "MIT", 22 | "main": "svelte-realtime-adapter-cosmos-db-temporal.js", 23 | "type": "module", 24 | "directories": { 25 | "test": "test" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-adapter-cosmos-db-temporal" 33 | }, 34 | "scripts": { 35 | "start": "cosmosdb-server -p 8081", 36 | "ports": "sudo netstat -ltnp", 37 | "test": "tape test/*.js | tap-spec" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/matrx-transformation/matrx/issues" 41 | }, 42 | "dependencies": { 43 | "@azure/cosmos": "^3.4.2", 44 | "tape": "^4.11.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/stored-procedures/operations.js: -------------------------------------------------------------------------------- 1 | module.exports = function(operationsSpec, session) { 2 | // Basic checks on all operations 3 | 4 | // Fetch access control data 5 | 6 | // .then 7 | // Check to see that all operations are allowed by access control data 8 | 9 | // .then 10 | // Send out requests to fetch all latest versions for temporal operations 11 | 12 | // .then 13 | // Check to see if all of the old values match the provided old values 14 | 15 | // .then 16 | // Do all the operations 17 | console.log('something') 18 | } 19 | -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/svelte-realtime-adapter-cosmos-db-temporal.js: -------------------------------------------------------------------------------- 1 | /* 2 | For MatrX use, we support temporal entities out of the box but if you would 3 | rather not, that's fine, just make sure your objects do not have _isTemporal:true. 4 | Your objects may come back with some extra stuff tacked on (like _entityID). 5 | 6 | If that extra stuff gives you heartburn, you can do something about it. 7 | All temporal functionality is in the adapter rather than the coordinator, which pretty 8 | much just moves your objects around. So, it is possible to write your own adapter without 9 | the temporal semantics. 10 | 11 | If an object is missing an id field, one will be added. If it's missing an _entityID 12 | a GUID will be added. If it's a non-temporal object (meaning it does not have _isTemporal:true) 13 | the _entityID will be set to the same value as the id field. When _isTemporal:true, any created 14 | _entityID will be a new GUID. 15 | */ 16 | 17 | const cosmos = require('@azure/cosmos') 18 | const fs = require('fs') 19 | const path = require('path') 20 | 21 | class Adapter { 22 | 23 | constructor() { 24 | console.log('Inside constructor of Adapter') 25 | this.upsertStoredProcedures() 26 | } 27 | 28 | upsertStoredProcedures() { 29 | const spFolder = path.join(__dirname, 'stored-procedures') 30 | const filenames = fs.readdirSync(spFolder) 31 | console.log(filenames) 32 | for (const filename of filenames) { 33 | const fullPath = path.join(spFolder, filename) 34 | console.log(fullPath) 35 | const body = require(fullPath) 36 | console.log(body.toString()) 37 | } 38 | } 39 | 40 | // Query returning the most current version of entities 41 | async query(query) { 42 | 43 | } 44 | 45 | // Fetch the most current version of an entity 46 | async read(entityID) { 47 | 48 | } 49 | 50 | /* 51 | The operationsSpec is a list of entities to create, delete, or update. 52 | Later, we may support patch (or other incremental operations that are based upon the current value) 53 | 54 | This can be a mix of operations on temporal and non-temporal entities. 55 | 56 | For this adapter at least, we plan to use Azure Cosmos DB stored procedures will means the list 57 | of operations are the equivalent of a relational database transaction meaning all succeed or 58 | all fail. 59 | 60 | You must always provide the entire new object. 61 | 62 | For updates and deletes, you must also provide the entire old object including the id. 63 | If the old version id in the database does not match the id of the provided old object, 64 | or if the old id comes back and it's not the latest version (_validTo=Infinity) 65 | the transaction will fail. This is how we implement optimistic concurrency. 66 | 67 | We want the entire old object so we can implement latency compensation. We'll keep the old 68 | version around while we simultaneously kick off realtime client updates and database operations. 69 | 70 | If the database operation fails because the old value was not the latest, the latest will be 71 | returned by the stored procedure and that will be broadcast. That may look like two glowing green 72 | around the value in rapid succession, but it should be rare enough not to be an annoyance. 73 | 74 | If the database operation fails for any other reason, we will send a revert event (with the 75 | old version (which may have come back from the sproc)) for all operations in the operationSet 76 | to all subscribed clients. 77 | 78 | We'll live with the slight risk of the app server rebooting before it can get a 79 | database operation fail. When the clients reconnect to the rebooted app server the join operation 80 | attempts to sync everything back up so it's hard for me to imagine a situation where 81 | the database and clients are out of sync for long... and it's a web app so a refresh fixes 82 | everything. 83 | 84 | [ 85 | 86 | // Example create non-temporal. id and _entityID will be created and the same 87 | {operationType: 'create', new: {a: 1}}, 88 | 89 | // Example create temporal. id and _entityID will be created and different 90 | {operationType: 'create', new: {_isTemporal: true, a: 1}}, 91 | 92 | // Example update non-temporal. old must have both id and _entityID fields, 93 | // id will be created for new object. Operation (and thus entire transaction) 94 | // will fail if old.id does not match version in the database (optimistic concurrency) 95 | {operationType: 'update', old: {id: 1234, _entityID: 'B78', b: 10}, new: {_entityID: 'B78', b: 20, c: 30}} 96 | 97 | // Example delete temporal. Provide entire old version for latency compensation 98 | {operationType: 'delete', old: {id: 5678, _entityID: 'C42', ...}} 99 | ] 100 | */ 101 | async operations(operationsSpec) { 102 | console.log('inside adapter.operations') 103 | // Call 'operations' stored procedure 104 | // What do we return if the call to the stored procedure fails? 105 | } 106 | 107 | } 108 | 109 | function getAdapter() { // TODO: Allow authentication to be passed in to overide the default of getting it from environment variables 110 | if (!adapter) { 111 | adapter = new Adapter() 112 | } 113 | return adapter 114 | } 115 | 116 | let adapter 117 | 118 | module.exports = {getAdapter} // TODO: Eventually change this to export once supported 119 | 120 | 121 | /* 122 | We wanted to provide an API endpoint to enable integration without requiring all 123 | API clients to use socket.io. This is required if we use Azure Functions to update 124 | materialized views. In theory, I guess it could be possible to have an Azure Function 125 | act as a socket.io client but I'm betting that we'll want the API endpoints anyway. 126 | 127 | */ 128 | 129 | class Coordinator { 130 | 131 | constructor(server, nsp, adapter, namespace) { 132 | this._namespace = namespace || Coordinator.DEFAULT_NAMESPACE 133 | this._server = server 134 | this._nsp = nsp 135 | this._adapter = adapter 136 | console.log('inside constructor of Coordinator') 137 | } 138 | 139 | async authenticate(username, password) { 140 | const authenticated = await this._adapter.authenticate(username, password) 141 | } 142 | 143 | async read(_entityID) { 144 | // TODO: Check to see if there is a cachedValue in the room and return that ASAP (more latency compensation) 145 | const value = await this._adapter.getEntity(_entityID) // TODO: Need a try-catch-throw here 146 | return value // We should really return this from the API call even if it is fire and forget from the store so non-store API calls get something back 147 | } 148 | 149 | async operations(operationsSpec, socketSessionID) { 150 | console.log('inside coordinator.operations') 151 | // TODO: Check operationsSpec. Failing for some things. Defaulting for others 152 | 153 | // Server-side latency compensation means we broadcast the operation to everyone but 154 | // the original sender as specified by socketID 155 | const socketLookup = this._namespace + '#' + socketSessionID 156 | const socket = this._nsp.sockets[socketLookup] 157 | for (const operation in operationsSpec) { 158 | // TODO: Check access control (cached?) before this latency compensation 159 | if (operation.operationType === 'delete') { 160 | console.log('do something here') 161 | } else { // operationType is 'create' or 'update' 162 | const value = operation.new 163 | const _entityID = value._entityID 164 | const storeID = JSON.stringify({_entityID}) 165 | const room = this._nsp.adapter.rooms[storeID] 166 | if (room) { 167 | room.cachedValue = value 168 | socket.to(storeID).emit('set', value) 169 | } 170 | } 171 | } 172 | 173 | // Call adapter.operations 174 | try { 175 | const result = await this._adapter.operations(operationsSpec) 176 | // TODO: Send "saved" event to component 177 | } catch { // If fail, send revert event to all subscribed 178 | for (const operation in operationsSpec) { 179 | if (operation.operationType === 'delete') { 180 | console.log('do something here also') 181 | } else { // operationType is 'create' or 'update' 182 | const value = operation.old 183 | const _entityID = value._entityID 184 | const storeID = JSON.stringify({_entityID}) 185 | const room = this._nsp.adapter.rooms[storeID] 186 | if (room) { 187 | room.cachedValue = value 188 | this._nsp.in(storeID).emit('revert', value) // TODO: Maybe we can just call emit() on the already fetched room? 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | } 196 | 197 | Coordinator.DEFAULT_NAMESPACE = '/svelte-realtime' 198 | 199 | function getCoordinator(server, nsp, adapter, namespace) { // TODO: Allow authentication to be passed in to overide the default of getting it from environment variables 200 | if (!coordinator) { 201 | coordinator = new Coordinator() 202 | } 203 | return coordinator 204 | } 205 | 206 | let coordinator 207 | 208 | // module.exports = {getCoordinator} // TODO: Eventually change this to export once supported 209 | -------------------------------------------------------------------------------- /packages/svelte-realtime-adapter-cosmos-db-temporal/test/single-entity-saves.spec.js: -------------------------------------------------------------------------------- 1 | 2 | const {default: cosmosServer} = require("@zeit/cosmosdb-server") 3 | const {CosmosClient} = require("@azure/cosmos") 4 | const https = require("https") 5 | const test = require('tape') 6 | 7 | const server = cosmosServer().listen(8081, () => { 8 | console.log(`Cosmos DB server running at https://localhost:8081`) 9 | runClient().catch(console.error) 10 | }) 11 | 12 | async function runClient() { 13 | const client = new CosmosClient({ 14 | endpoint: `https://localhost:8081`, 15 | key: "dummy key", 16 | // disable SSL verification 17 | // since the server uses self-signed certificate 18 | agent: https.Agent({rejectUnauthorized: false}) 19 | }) 20 | 21 | // initialize databases since the server is always empty when it boots 22 | const {database} = await client.databases.createIfNotExists({id: 'test-db'}) 23 | const {container} = await database.containers.createIfNotExists({id: 'test-container'}) 24 | 25 | // use the client 26 | try { 27 | return await tests(container) 28 | } catch (e) { 29 | console.log('got error on tests') 30 | throw (e) 31 | } finally { 32 | await server.close() 33 | // await new Promise((resolve) => { 34 | // server.close(resolve) 35 | // }) 36 | } 37 | 38 | } 39 | 40 | async function tests(container) { 41 | test('timing test', async (t) => { 42 | 43 | t.equal(1, 1) 44 | 45 | const o = container.items.upsert({a: 1}) 46 | // const o = await container.items.upsert({a: 1}) 47 | // t.deepEqual(o, {a: 1}) 48 | 49 | t.end() 50 | 51 | }) 52 | } 53 | 54 | // return 55 | -------------------------------------------------------------------------------- /packages/svelte-realtime-server/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Larry Maccherone 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /packages/svelte-realtime-server/README.md: -------------------------------------------------------------------------------- 1 | # `@matrx/svelte-realtime-server` 2 | 3 | svelte-realtime-server is the server-side companion to the [`svelte-realtime-store`](https://www.npmjs.com/package/@matrx/svelte-realtime-store). Go over there for a summary of what it does, features, usage, limitations, etc. 4 | 5 | ## Usage 6 | 7 | To install with npm 8 | 9 | ```bash 10 | npm install --save @matrx/svelte-realtime-server 11 | ``` 12 | 13 | Modify your server.js file to look something like this 14 | 15 | ```js 16 | import http from 'http' 17 | import sirv from 'sirv' 18 | import express from 'express' 19 | import compression from 'compression' 20 | 21 | // Look here 22 | import { getServer } from '@matrx/svelte-realtime-server' 23 | 24 | const { PORT, NODE_ENV } = process.env 25 | const dev = NODE_ENV === 'development' 26 | 27 | const app = express() 28 | const server = http.createServer(app) 29 | 30 | // And here 31 | const svelteRealtimeServer = getServer(server, adapters, sessionStore) 32 | 33 | app.use( 34 | compression({ threshold: 0 }), 35 | sirv('static', { dev }), 36 | ) 37 | 38 | server.listen(PORT, err => { 39 | if (err) console.log('error', err); 40 | }) 41 | 42 | ``` 43 | 44 | ### Authentication 45 | 46 | svelte-realtime-server now requiers a sessionID cookie to be present when the browser first connects. The `server.js` file in the parent folder is a fully working example of how you can do this complete with example /login, /logout, and /checkauth endpoints. At this time, we require you to pass in the sessionStore as the third parameter but this will later be upgraded to permit you to pass in your own callback functions. The second parameter, `adapters` is for the as-yet incomplete database serialization functionality and you can safely pass in `null` at this time. 47 | 48 | ### Access control 49 | 50 | TBD -------------------------------------------------------------------------------- /packages/svelte-realtime-server/__tests__/realtime.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const realtime = require('..'); 4 | 5 | describe('@matrx/svelte-realtime-store', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/svelte-realtime-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-realtime-server", 3 | "version": "0.6.0", 4 | "description": "Server-side middleware for building realtime Svelte apps", 5 | "keywords": [ 6 | "realtime", 7 | "real-time", 8 | "sapper", 9 | "svelte", 10 | "svelte3", 11 | "store", 12 | "socket.io", 13 | "websocket" 14 | ], 15 | "author": "Larry Maccherone ", 16 | "homepage": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-server#readme", 17 | "license": "MIT", 18 | "main": "svelte-realtime-server.cjs", 19 | "directories": { 20 | "test": "__tests__" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-server" 28 | }, 29 | "scripts": { 30 | "test": "echo \"Error: run tests from root\" && exit 1" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/matrx-transformation/matrx/issues" 34 | }, 35 | "dependencies": { 36 | "socket.io": "^2.3.0", 37 | "svelte": "^3.23.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/svelte-realtime-server/svelte-realtime-server.cjs: -------------------------------------------------------------------------------- 1 | const socketIO = require('socket.io') 2 | const debug = require('debug')('matrx:svelte-realtime-server') 3 | const cookie = require('cookie') 4 | const cookieParser = require('cookie-parser') 5 | 6 | const DEFAULT_NAMESPACE = '/svelte-realtime' 7 | 8 | const {SESSION_SECRET} = process.env 9 | if (!SESSION_SECRET) { 10 | throw new Error('Must set SESSION_SECRET environment variable') 11 | } 12 | 13 | function getServer(server, adapters, sessionStore, namespace = DEFAULT_NAMESPACE) { 14 | const io = socketIO(server, {pingTimeout: 30000, pingInterval: 20000}) 15 | // const io = socketIO(server) 16 | const nsp = io.of(namespace) 17 | const lookupSocketsBySessionID = new Map() // {sessionID: [socket]} 18 | 19 | nsp.use((socket, next) => { 20 | const rawCookies = socket.request.headers.cookie 21 | const parsedCookies = cookie.parse(rawCookies) 22 | const sessionID = cookieParser.signedCookie(parsedCookies.sessionID, SESSION_SECRET) 23 | debug('socket.io middleware called. sessionID: %O', sessionID) 24 | if (sessionID) { 25 | sessionStore.get(sessionID, (err, session) => { 26 | debug('sessionStore.get callback called. session: %O', session) 27 | if (err) { 28 | return next(new Error('not authorized')) 29 | } else { 30 | socket.sessionID = sessionID 31 | if (!lookupSocketsBySessionID.get(sessionID)) { 32 | lookupSocketsBySessionID.set(sessionID, new Set()) 33 | } 34 | lookupSocketsBySessionID.get(sessionID).add(socket) 35 | return next() 36 | } 37 | }) 38 | } else { 39 | return next(new Error('not authorized')) 40 | } 41 | }) 42 | 43 | nsp.on('connect', (socket) => { 44 | debug('connect msg received') 45 | 46 | socket.on('disconnect', () => { 47 | debug('disconnect msg received') 48 | if (socket.sessionID) { 49 | const socketSet = lookupSocketsBySessionID.get(socket.sessionID) 50 | if (socketSet) { 51 | socketSet.delete(socket) 52 | if (socketSet.size == 0) { 53 | lookupSocketsBySessionID.delete(socket.sessionID) 54 | } 55 | } 56 | } 57 | }) 58 | 59 | socket.on('join', (stores) => { // TODO: Check access control before joining 60 | debug('join msg received. stores: %O', stores) 61 | for (const {storeID, value} of stores) { 62 | socket.join(storeID) 63 | const room = nsp.adapter.rooms[storeID] 64 | if (room) { // There should always be a room but better safe 65 | const cachedValue = room.cachedValue 66 | if (cachedValue) { 67 | socket.emit('set', storeID, cachedValue) // This sends only the originator 68 | } else { 69 | room.cachedValue = value 70 | // TODO: Think about switching below for efficiency. It's not done that way for now to be extra safe and because I wasn't sure when there are multiple stores on the same page that it would work 71 | // socket.to(storeID).emit('set', storeID, value) // This sends to all clients except the originating client 72 | nsp.in(storeID).emit('set', storeID, value) // This sends to all clients including the originator 73 | } 74 | } else { 75 | throw new Error('Unexpected condition. There should be one but there is no room for storeID: ' + storeID) 76 | } 77 | } 78 | }) 79 | 80 | socket.on('set', (storeID, value, forceEmitBack) => { 81 | debug('set msg received. storeID: %s value: %O', storeID, value) 82 | 83 | // Latency compensation by optimistically sending update to room 84 | let room = nsp.adapter.rooms[storeID] 85 | if (!room) { 86 | socket.join(storeID) 87 | room = nsp.adapter.rooms[storeID] 88 | } 89 | if (room) { // There should always be a room now but better safe 90 | room.cachedValue = value 91 | } else { 92 | throw new Error('Unexpected condition. There should be one but there is no room for storeID: ' + storeID) 93 | } 94 | if (forceEmitBack) { 95 | nsp.in(storeID).emit('set', storeID, value) // This sends to all clients including the originator 96 | } else { 97 | socket.to(storeID).emit('set', storeID, value) // This sends to all clients except the originating client 98 | } 99 | 100 | // TODO: Save to database via adapter 101 | 102 | // If database save fails, then revert 103 | const databaseSaveFailed = false // TODO: Drive this off of above 104 | if (databaseSaveFailed) { 105 | const room = nsp.adapter.rooms[storeID] 106 | if (room && room.cachedValue) { 107 | nsp.in(storeID).emit('revert', storeID, room.cachedValue) // This sends to all clients including the originator 108 | } 109 | } 110 | }) 111 | }) 112 | 113 | function logout(sessionID) { 114 | debug('svelte-realtime-server.logout() called. sessionID: %O', sessionID) 115 | if (lookupSocketsBySessionID.get(sessionID)) { 116 | lookupSocketsBySessionID.get(sessionID).forEach((tempSocket) => { 117 | tempSocket.disconnect() 118 | }) 119 | lookupSocketsBySessionID.delete(sessionID) 120 | } 121 | } 122 | 123 | return {nsp, logout} 124 | } 125 | 126 | module.exports = {getServer} // TODO: Eventually change this to export once supported 127 | -------------------------------------------------------------------------------- /packages/svelte-realtime-store/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Larry Maccherone 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /packages/svelte-realtime-store/README.md: -------------------------------------------------------------------------------- 1 | # `@matrx/svelte-realtime-store` 2 | 3 | svelte-realtime-store is a drop-in replacement for a Svelte writable store that will synchronize its value across browser windows/tabs in realtime using socket.io. 4 | 5 | If you currently have a svelte app that communicates with a node.js server (Sapper or otherwise), you can upgrade your existing application to [Meteor](https://www.meteor.com/) or [Firebase](https://firebase.google.com/) like realtime literally in a few minutes. 6 | 7 | To work, you must have the [`@matrx/svelte-realtime-server`](https://www.npmjs.com/package/@matrx/svelte-realtime-server) running on your node.js server. 8 | 9 | ### Features 10 | 11 | * Requires only a few additional lines of code 12 | * Implements the expected interface of a Svelte writable store so Svelte's wonderful syntax just works 13 | * Caches the value on the server so newly joined and reconnected users catch up to the current state 14 | * Exposes a Svelte readable store for the connection status so you can decide how it should behave when offline. I recommend that you disable user input when offline unless you implement your own offline mode (including conflict resolution upon reconnect). 15 | * Uses a socket.io namespace to isolate it from anything else you might be using socket.io for 16 | * Exposes the socket.io namespaced socket to your application for advanced use cases 17 | 18 | ## Usage 19 | 20 | To install with npm 21 | 22 | ```bash 23 | npm install --save @matrx/svelte-realtime-store 24 | ``` 25 | 26 | Head on over to [`@matrx/svelte-realtime-server`](https://www.npmjs.com/package/@matrx/svelte-realtime-server) to see how to install the server-side component. 27 | 28 | Modify your .svelte files to look something like this 29 | 30 | ```html 31 | 42 | 43 |

{$a}

44 | 45 | ``` 46 | 47 | One last step. You'll need to make the socket.io client available to the page. The easiest way to do that is to include this in your `index.html` file. 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | This works because the socket.io server serves up its own version-matched client on that endpoint when installed on your server either stand-alone or when included as part of the svelte-realtime-server. 54 | 55 | Alternatively, you can install the client as a seperate npm package and include it. See the socket.io documentation for details on how to do that. 56 | 57 | ### Notes 58 | 59 | * Notice how I've used the provided `realtimeClient.connected` store to disable the button when the window is disconnected. This is recommended unless you implement your own offline mode (including conflict resolution upon reconnection). 60 | * The first parameter of `realtimeClient.realtime()` can be a simple string which will act as a unique identifier for the store which corresponds exactly to the name of the "room" in socket.io parlance. 61 | * If you are building a simple single user-base app, then you can do as I've done above and make the store id match the variable name. 62 | * However, for multi-tenancy or other cases where you want the `a` variable for one page/user/tenant to be isolated from the `a` variable for another, you'll need to provide a different id for each seperate instance. This can be done explicitly by proving an object with a `storeID` field (e.g. `realtimeClient.realtime({storeID: 'something'}, 1000)`). However, if your object lacks a `storeID` field it will run JSON.stringify() on it so something like this works using the Sapper-provided page store: `realtimeClient.realtime({page, variable: 'a'}, 1000)`. If you are not using Sapper and don't have to worry about server-side rendering (SSR), you can just do: `realtimeClient.realtime({page: window.location.href, variable: 'a'})` 63 | * I'm getting ready to add serialization to a database upon store updates so you may see examples of `_entityID` rather than `storeID`. I'll come back later, once I have that working and explain that option. 64 | * The second parameter of `realtimeClient.realtime()` is now the _default_ value rather than the _initial_ value. What this means is that if the server has cached a value for this store, it'll start with that value rather than the one you provide. 65 | * Keep in mind that socket.io is very efficient at cleaning up "rooms" when there are no clients. So, the value will be flushed from the cache when all clients disconnect. The default or last browser update value will repopulate the cache upon reconnection. 66 | 67 | ### API 68 | 69 | ```js 70 | realtime(storeConfig, default_value, component = null, start = noop) 71 | ``` 72 | 73 | #### storeConfig options 74 | 75 | `storeConfig` can be a string like in the simple example above or it can be an object. If, it's an object and it contains a `storeID` field, that will be the name of the socket.io room used to identify other stores to synchronize to. If there is no `storeID` field, it'll fall back first to the `_entityID` field, if present and failing all that, the results of `JSON.stringify(storeConfig)` will be used as the storeID. 76 | 77 | The `debounceWait` field if present is the number of milliseconds to wait before sending the changed value to the server for synchronization. Use this for text input fields or any other incrementally altered field to prevent your application from being overly chatty. 78 | 79 | svelte-realtime-store allows you to have multiple stores on the same page that connect to the same synchronized room. In these cases, the store will update local copies immdiately ignoring the `debounceWait`. 80 | 81 | ## Advanced usage 82 | 83 | We expose the namespaced socket.io socket if you need it for more advanced uses. 84 | 85 | For instance, under the covers, the `realtimeClient.connected` store is maintaining its state with something like this: 86 | 87 | ```js 88 | realtimeClient.socket.on('disconnect', () => { 89 | console.log('You are no longer connected!') 90 | }) 91 | realtimeClient.socket.on('connect', () => { 92 | console.log('You are now connected!') 93 | }) 94 | ``` 95 | 96 | ## Limitations 97 | 98 | * __No permanent storage.__ As of this writing, it just synchronizes the views but the next step is to provide an adapter interface to save your data in the database of your choice. For now, you still need to take care of saving your data when it changes. 99 | * __Beta.__ As of this writing, this package is under active development and using semver conventions for beta projects. Minor-level upgrades may be backward breaking while patch-level upgrades will be used for changes that are not backward breaking. 100 | * __Svelte 3 only.__ Unless I'm mistaken, the store API is completely different in Svelte 2. 101 | -------------------------------------------------------------------------------- /packages/svelte-realtime-store/__tests__/realtime.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const realtime = require('..'); 4 | 5 | describe('@matrx/svelte-realtime-store', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/svelte-realtime-store/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-realtime-store", 3 | "version": "0.6.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lodash": { 8 | "version": "4.17.15", 9 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 10 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/svelte-realtime-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-realtime-store", 3 | "version": "0.6.0", 4 | "description": "Browser-side stores and server-side middleware for building realtime Svelte apps", 5 | "keywords": [ 6 | "realtime", 7 | "real-time", 8 | "sapper", 9 | "svelte", 10 | "svelte3", 11 | "store", 12 | "socket.io", 13 | "websocket" 14 | ], 15 | "author": "Larry Maccherone ", 16 | "homepage": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-store#readme", 17 | "license": "MIT", 18 | "main": "svelte-realtime-store.js", 19 | "type": "module", 20 | "directories": { 21 | "test": "__tests__" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-realtime-store" 29 | }, 30 | "scripts": { 31 | "test": "echo \"Error: run tests from root\" && exit 1" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/matrx-transformation/matrx/issues" 35 | }, 36 | "dependencies": { 37 | "lodash": "^4.17.15" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/svelte-realtime-store/svelte-realtime-store.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('matrx:svelte-realtime-store') 2 | const {debounce} = require('lodash') 3 | 4 | import {onDestroy} from 'svelte' // TODO: Either use this or remove it 5 | import {writable} from 'svelte/store' 6 | 7 | debug('svelte-realtime-store loads and initializes') 8 | const namespace = '/svelte-realtime' 9 | let socket = null 10 | const stores = {} // {storeID: [store]} 11 | const components = {} // {storeID: component} // TODO: Need to upgrade this to an array like stores 12 | 13 | export class RealtimeStore { 14 | constructor(storeConfig) { 15 | this.storeConfig = storeConfig || {} 16 | this.storeID = storeConfig.storeID || storeConfig._entityID || JSON.stringify(storeConfig) 17 | this.component = storeConfig.component || null 18 | this.debounceWait = storeConfig.debounceWait || 0 19 | this.forceEmitBack = storeConfig.forceEmitBack || false // added to enable single-page testing 20 | this.ignoreLocalSet = storeConfig.ignoreLocalSet || false // added to enable single-page testing 21 | this.defaultValue = storeConfig.defaultValue || null 22 | 23 | this.lastNewValue = null 24 | 25 | this.wrappedStore = writable(this.defaultValue) 26 | this.subscribe = this.wrappedStore.subscribe 27 | 28 | this._emitSet = this._emitSet.bind(this) 29 | this._debouncedEmit = debounce(this._emitSet, this.debounceWait).bind(this) 30 | 31 | if (this.component) { 32 | components[this.storeID] = this.component 33 | } 34 | if (!stores[this.storeID]) { 35 | stores[this.storeID] = [] 36 | } 37 | stores[this.storeID].push(this) 38 | } 39 | 40 | _emitSet() { // TODO: This may need .bind(this) in constructor 41 | debug('emitSet() called. storeID: %O, lastNewValue: %O, forceEmitBack: %O', this.storeID, this.lastNewValue, this.forceEmitBack) 42 | if (socket) { 43 | socket.emit('set', this.storeID, this.lastNewValue, this.forceEmitBack) 44 | } 45 | } 46 | 47 | _debouncedEmit() { // TODO: Need to really debounce 48 | this._emitSet() 49 | } 50 | 51 | set(newValue) { 52 | debug('inside set(). this.storeID: %O', this.storeID) 53 | debug('inside set(). newValue: %O', newValue) 54 | this.lastNewValue = newValue 55 | if (this.debounceWait) { 56 | this._debouncedEmit() 57 | } else { 58 | this._emitSet() 59 | } 60 | stores[this.storeID].forEach((store) => { 61 | if (!store.ignoreLocalSet) { 62 | store.wrappedStore.set(newValue) 63 | } 64 | }) 65 | } 66 | 67 | update(fn) { 68 | let newValue 69 | this.wrappedStore.update((currentValue) => { 70 | newValue = fn(currentValue) 71 | return newValue 72 | }) 73 | this.set(newValue) 74 | } 75 | 76 | } 77 | 78 | RealtimeStore.connected = writable(false) 79 | 80 | RealtimeStore.afterAuthenticated = function(callback) { // TODO: Does this need to be an => function? 81 | try { 82 | debug('afterAutenticated() called') 83 | socket.on('set', (storeID, value) => { 84 | debug('set msg received. storeID: %s value: %O', storeID, value) 85 | stores[storeID].forEach((store) => { 86 | store.wrappedStore.set(value) 87 | }) 88 | }) 89 | socket.on('revert', (storeID, value) => { 90 | debug('revert msg received. storeID: %s value: %O', storeID, value) 91 | stores[storeID].forEach((store) => { 92 | store.wrappedStore.set(value) 93 | // TODO: Send "revert" event to each component 94 | }) 95 | }) 96 | socket.on('saved', (storeID) => { 97 | debug('set msg received. storeID: %s', storeID) 98 | // TODO: Send "saved" event to each component 99 | }) 100 | const storesReshaped = [] 101 | for (const storeID in stores) { 102 | stores[storeID][0].wrappedStore.update((value) => { 103 | storesReshaped.push({storeID, value}) 104 | }) 105 | } 106 | socket.emit('join', storesReshaped) 107 | RealtimeStore.connected.set(true) 108 | callback(true) 109 | } catch (e) { 110 | if (e instanceof RangeError) { 111 | location.reload() 112 | // throw e 113 | } 114 | } 115 | } 116 | 117 | RealtimeStore.restoreConnection = function(callback) { // TODO: Does this need to be an => function? 118 | try { 119 | debug('restoreConnection() called') 120 | if (socket) { 121 | socket.removeAllListeners() 122 | socket = null 123 | } 124 | socket = io(namespace) 125 | socket.on('connect', () => { 126 | debug('connect msg received') 127 | socket.on('disconnect', () => { 128 | debug('disconnect msg received') 129 | RealtimeStore.connected.set(false) 130 | socket.removeAllListeners() 131 | socket.on('reconnect', () => { 132 | debug('reconnect msg received') 133 | RealtimeStore.restoreConnection(() => { 134 | debug('Got response to call to restoreConnection() from inside reconnect event. Ignoring.') 135 | }) 136 | }) 137 | }) 138 | RealtimeStore.afterAuthenticated(callback) 139 | }) 140 | } catch (e) { 141 | if (e instanceof RangeError) { 142 | location.reload() 143 | // throw e 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Ben Smithett 2 | 3 | Copyright (c) 2020 Larry Maccherone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/README.md: -------------------------------------------------------------------------------- 1 | # `@matrx/svelte-viewstate-store` 2 | 3 | `@matrx/svelte-viewstate-store` is a drop-in replacement for Svelte's writable 4 | store that keeps the URL in sync with any variables that determine the view. It 5 | also saves/restores the last view state from LocalStorage. The store value, the 6 | variable in the querystring, and the value saved in LocalStorage will all stay in 7 | sync. 8 | 9 | One aspect of a great UI is that a user can cut and paste the URL and send it to 10 | someone else, and, assuming they have the right permissions, they will see exactly 11 | what the first user saw. This is partially achieved by pulling the data from a 12 | database but that doesn't help for variables that the user manipulates to control 13 | their view. For example, if you have a report page where the user can specify a 14 | number of filter parameters, its desirable for the URL to contain the filter 15 | parameters. Another example might be if you have a carosel over 10 different 16 | pictures, you want the URL to specify which picture is active. Say 17 | `/carosel-page?activePicID=3`. 18 | 19 | Another aspect of a good UI is that when a user returns to a given page, they 20 | want the variables that control the view to generally default to the 21 | last values the user set and then fallback to some default... unless, of course, 22 | they got to the page with all the view variables in the URL as described in the 23 | prior paragraph. 24 | 25 | `@matrx/svelte-viewstate-store` helps with both of these. First, whenever 26 | view-state variables are updated by your code, the local 27 | querystring is automatically updated. Second, it saves the last user-set values 28 | to LocalStorage (for example, if the user hits the "Pan Right" button, it 29 | might change the activePicID to 4) for restoration the next 30 | time the user is sent to this page without these view-state variables like 31 | from a main menu with `href="/carosel-page"`. In this case, it'll read from 32 | LocalStorage that the last activePicID was 4 and it'll instantly update the URL 33 | to `/carosel-page?activePicID=4` and start at pic #4 in the carosel. 34 | 35 | ## Installation 36 | 37 | To install with npm 38 | 39 | ```bash 40 | npm install --save-dev @matrx/svelte-viewstate-store 41 | ``` 42 | 43 | ## Usage 44 | 45 | Create a store on the page you want it scoped to. If you want a globally 46 | scoped one, create it in `stores.js` and import it whereever you need it 47 | to drive the view off of. 48 | 49 | ```JavaScript 50 | import {ViewstateStore} from '@matrx/svelte-viewstate-store' 51 | 52 | const activePicID = new ViewstateStore({ 53 | identifier: 'activePicID', 54 | defaultValue: 0, 55 | type: 'Int', // Also accepts 'Float' and 'Boolean'. Defaults to 'String'. 56 | updateLocalStorageOnURLChange: true, // Defaults to false 57 | isGlobal: true // Defaults to false 58 | }) 59 | ``` 60 | 61 | ### `storeConfig` 62 | 63 | * `identifier` - It's usually best to have this equal the name of the 64 | variable. The scope (see below) is used to prefix this when 65 | saving/restoring to/from LocalStorage so there is minimal risk of 66 | conflicts. 67 | 68 | * `defaultValue` - This is only used whenever the variable is not in the 69 | querystring nor LocalStorage, like when the first time this user ever 70 | visits the page from a menu. 71 | 72 | * `type` - 'Int', 'Float', or 'Boolean'. Defaults to 'String'. 73 | 74 | * `scope` - By default, the current route as specified by 75 | svelte-spa-router's `location` store when the ViewstateStore is 76 | instantiated is used to define the "scope" that this variable is 77 | active. If you navigate away from this scope, the variables will 78 | cease to update the URL even before the onDestroy callback has a 79 | chance to remove the subscriptions. 80 | 81 | This works fine for leaf node locations but 82 | not for variables attached to the root of your app or parents in nested 83 | routes. Use this `scope` config option if you want to specify some other 84 | scope. For instance, let's say you have a global `teamID` variable 85 | that is used in many pages. You want `#/?teamID=team1`, but you also want 86 | `#/my-page?teamID=team1&someOtherVariable=10`. You accomplish this by 87 | specifying `scope:'/'` in your storeConfig. 88 | 89 | You may also choose to use this option if you want the querystring variables 90 | to be stable across all possible values of parameterized routes like 91 | `/posts/:author/:slug`. In this case, specify `scope:'/posts'` in 92 | your storeConfig. 93 | 94 | Note: the code uses a simple `string.startsWith()` to determine if the 95 | variable is still in scope or not. This would not work with any 96 | regex routes. 97 | 98 | * `updateLocalStorageOnURLChange` - The default is to only update 99 | LocalStorage when changed by your code which is usually in response 100 | to some explicit user action in the UI. You can override this behavior and 101 | update LocalStorage even on URL change by setting this config item to 102 | `true`. 103 | 104 | * `isGlobal` - Normally, we create these stores inside components. 105 | However, if you use the Svelte `stores.js` convention, then you should 106 | set this to `true`. For now, the only thing this does is supress the 107 | onDestroy behavior. This is a potential memory leak but if you are 108 | instantiating things in a global stores.js, you have a bit of memory inefficiency 109 | already. 110 | 111 | activePicID can now be used as you would other Svelte writable store -- in 112 | reactive code like this: 113 | 114 | ```JavaScript 115 | $: nextPic = $activePicID + 1 116 | ``` 117 | 118 | Or in a callback for a UI action like this: 119 | 120 | ```HTML 121 | 130 | 131 | 132 | 133 | ``` 134 | 135 | ## Warnings 136 | 137 | * `@matrx/svelte-viewstate-store` relies upon `svelte-spa-router` and it 138 | has only been tested with it. If you are using another router, it will 139 | probably not work. 140 | 141 | * At the moment, the `scope` storeConfig option uses `string.startsWith()` 142 | so it only works for string specified routes (e.g. '/pictures-page') but 143 | not regex routes. I have an idea on how to support this but I personally 144 | never use regex routes so it's hard to justify the work. 145 | 146 | * As the data and your app evolve, the values in LocalStorage and bookmarked 147 | URLs may become invalid. For instance, what if we've saved activePicID=10 148 | in LocalStorage but there are now only 8 pictures. No telling how your page 149 | will act. So, it's a good idea to validate the values right after you 150 | instantiate a ViewstateStore. The good news is that svelte stores update as 151 | soon as they are instantiated. So, you can validate and adjust them even 152 | before the first render. So, for our example, you could do this: 153 | 154 | ```JavaScript 155 | const activePicID = new ViewstateStore({ 156 | identifier: 'activePicID', 157 | defaultValue: 0, 158 | type: 'Int' 159 | }) 160 | $activePicID = Math.min(pictures.length - 1, $activePicID) 161 | ``` 162 | -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/__tests__/svelte-viewstate-store.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {ViewstateStore} = require('../svelte-viewstate-store') 4 | 5 | describe('@matrx/svelte-viewstate-store', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-viewstate-store", 3 | "version": "0.6.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "svelte": { 8 | "version": "3.22.3", 9 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.22.3.tgz", 10 | "integrity": "sha512-DumSy5eWPFPlMUGf3+eHyFSkt5yLqyAmMdCuXOE4qc5GtFyLxwTAGKZmgKmW2jmbpTTeFQ/fSQfDBQbl9Eo7yw==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matrx/svelte-viewstate-store", 3 | "version": "0.6.0", 4 | "description": "Drop-in replacement for Svelte's writable store that keeps the URL in sync with any variables that determine the view and saves/restores last view state from LocalStorage", 5 | "keywords": [ 6 | "LocalStorage", 7 | "svelte", 8 | "svelte3", 9 | "store" 10 | ], 11 | "author": "Larry Maccherone ", 12 | "homepage": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-viewstate-store#readme", 13 | "license": "MIT", 14 | "main": "svelte-viewstate-store.js", 15 | "type": "module", 16 | "directories": { 17 | "test": "__tests__" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/matrx-transformation/matrx/tree/master/packages/svelte-viewstate-store" 25 | }, 26 | "scripts": { 27 | "test": "echo \"Error: run tests from root\" && exit 1" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/matrx-transformation/matrx/issues" 31 | }, 32 | "devDependencies": { 33 | "svelte": "^3.22.3", 34 | "svelte-spa-router": "^1.3.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/svelte-viewstate-store/svelte-viewstate-store.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('matrx:svelte-viewstate-store') 2 | 3 | import {onDestroy} from 'svelte' 4 | import {writable} from 'svelte/store' 5 | import {push, loc} from 'svelte-spa-router' 6 | 7 | export class ViewstateStore { 8 | constructor(storeConfig) { 9 | this.storeConfig = storeConfig 10 | this.scope = storeConfig.scope 11 | 12 | this.wrappedStore = writable(storeConfig.defaultValue) 13 | this.subscribe = this.wrappedStore.subscribe 14 | 15 | // this.wrappedStore.set(storeConfig.defaultValue) // This is just so value is not undefined. It gets updated by the querystring or LocalStorage 16 | 17 | this.onURLChange = this.onURLChange.bind(this) 18 | const unsubscribe = loc.subscribe(this.onURLChange) 19 | 20 | if (!this.storeConfig.isGlobal) { 21 | onDestroy(unsubscribe) 22 | } 23 | } 24 | 25 | getQuerystringParam(querystring) { 26 | const urlSearchParams = new URLSearchParams(querystring) 27 | const valueString = urlSearchParams.get(this.storeConfig.identifier) 28 | if (valueString === null) { 29 | return {newValue: null, urlSearchParams} 30 | } 31 | let newValue = valueString 32 | if (this.storeConfig.type === 'Float') { // TODO: Support arrays of values 33 | newValue = Number.parseFloat(valueString) 34 | } else if (this.storeConfig.type === 'Int') { 35 | newValue = +valueString // Prefer over Number.parseInt(valueString, 10) because it returns NaN for "1 abc" 36 | // newValue = Number.parseInt(valueString, 10) 37 | } else if (this.storeConfig.type === 'Boolean') { 38 | newValue = (valueString == 'true') 39 | } 40 | return {newValue, urlSearchParams} 41 | } 42 | 43 | onURLChange(newLoc) { 44 | this.scope = this.scope || newLoc.location 45 | this.location = newLoc.location 46 | 47 | if (!this.location.startsWith(this.scope)) { // TODO: This needs to work for parent/child routes 48 | return 49 | } 50 | 51 | let {newValue} = this.getQuerystringParam(newLoc.querystring) 52 | if (newValue != null) { 53 | if (this.storeConfig.updateLocalStorageOnURLChange) { 54 | window.localStorage[this.scope + '.' + this.storeConfig.identifier] = newValue 55 | } 56 | } else { 57 | newValue = window.localStorage[this.scope + '.' + this.storeConfig.identifier] || this.storeConfig.defaultValue 58 | ViewstateStore.queueURLUpdate(this.storeConfig.identifier, newValue) 59 | } 60 | this.wrappedStore.set(newValue) 61 | } 62 | 63 | set(newValue) { 64 | this.wrappedStore.set(newValue) 65 | window.localStorage[this.scope + '.' + this.storeConfig.identifier] = newValue 66 | ViewstateStore.queueURLUpdate(this.storeConfig.identifier, newValue) 67 | } 68 | 69 | update(fn) { 70 | let newValue 71 | this.wrappedStore.update((currentValue) => { 72 | newValue = fn(currentValue) 73 | return newValue 74 | }) 75 | window.localStorage[this.scope + '.' + this.storeConfig.identifier] = newValue 76 | ViewstateStore.queueURLUpdate(this.storeConfig.identifier, newValue) 77 | } 78 | // update(fn) { 79 | // let newValue 80 | // this.wrappedStore.update((currentValue) => { 81 | // newValue = fn(currentValue) 82 | // }) 83 | // this.set(newValue) 84 | // } 85 | 86 | } 87 | 88 | function getLocation() { 89 | const hashPosition = window.location.href.indexOf('#/') 90 | let location = (hashPosition > -1) ? window.location.href.substr(hashPosition + 1) : '/' 91 | // Check if there's a querystring 92 | const qsPosition = location.indexOf('?') 93 | let querystring = '' 94 | if (qsPosition > -1) { 95 | querystring = location.substr(qsPosition + 1) 96 | location = location.substr(0, qsPosition) 97 | } 98 | const urlSearchParams = new URLSearchParams(querystring) 99 | return {urlSearchParams, location, querystring} 100 | } 101 | 102 | ViewstateStore.pendingURLUpdates = {} 103 | ViewstateStore.timer = null 104 | 105 | ViewstateStore.queueURLUpdate = (key, value) => { 106 | debug('in ViewstateStore.queueURLUpdate. key: value %O: %O', key, value) 107 | if (ViewstateStore.timer !== null) { 108 | clearTimeout(ViewstateStore.timer) 109 | ViewstateStore.timer = null 110 | } 111 | ViewstateStore.pendingURLUpdates[key] = value 112 | ViewstateStore.timer = setTimeout(ViewstateStore.processPendingURLUpdates, 0) 113 | } 114 | 115 | ViewstateStore.processPendingURLUpdates = function() { 116 | const {urlSearchParams, location} = getLocation() 117 | const currentURLSearchString = urlSearchParams.toString() 118 | for (const [key, value] of Object.entries(ViewstateStore.pendingURLUpdates)) { 119 | urlSearchParams.set(key, value) 120 | } 121 | const newURLSearchString = urlSearchParams.toString() 122 | if (newURLSearchString !== currentURLSearchString) { 123 | push(location + '?' + newURLSearchString) 124 | } 125 | ViewstateStore.pendingURLUpdates = {} 126 | ViewstateStore.timer = null 127 | } 128 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | MatrX{$location} 39 | 40 | 41 | 42 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | 3 | const app = new App({ 4 | target: document.body 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /src/matrx.scss: -------------------------------------------------------------------------------- 1 | 2 | // For a complete list of variables: https://bulma.io/documentation/customize/variables/ 3 | 4 | @charset "utf-8"; 5 | 6 | // import bulmaswatch (must happen before importing bulma) 7 | @import "node_modules/bulmaswatch/lux/_variables.scss"; 8 | 9 | //adds color grey-lightest 10 | $grey-lighter: lighten($grey, 30%); 11 | 12 | // adjusts height of nav-bar (must happen before importing bulma) 13 | $navbar-height: 3rem; 14 | $navbar-breakpoint: 0px; 15 | $box-padding: 5rem; 16 | $box-radius: 10px; 17 | 18 | // Import only what you need from Bulma 19 | // @import "../node_modules/bulma/sass/utilities/_all.sass"; 20 | // @import "../node_modules/bulma/sass/base/_all.sass"; 21 | // @import "../node_modules/bulma/sass/elements/button.sass"; 22 | // @import "../node_modules/bulma/sass/elements/container.sass"; 23 | // @import "../node_modules/bulma/sass/elements/title.sass"; 24 | // @import "../node_modules/bulma/sass/form/_all.sass"; 25 | // @import "../node_modules/bulma/sass/components/navbar.sass"; 26 | // @import "../node_modules/bulma/sass/layout/hero.sass"; 27 | // @import "../node_modules/bulma/sass/layout/section.sass"; 28 | 29 | // Imports all of Bulma // TODO: Optimize to only the parts I later need 30 | @import "../node_modules/bulma/bulma.sass"; 31 | 32 | @import '../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass'; 33 | 34 | @import "../node_modules/bulma-badge/src/sass/index.sass"; 35 | 36 | @import "../node_modules/bulmaswatch/lux/_overrides.scss"; 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('matrx:router') 2 | 3 | import {location} from 'svelte-spa-router' 4 | import {derived, get} from 'svelte/store' 5 | import {readyToGo} from './stores' 6 | 7 | // Real Routes 8 | import Home from './routes/Home' 9 | import Login from './routes/Login' 10 | import Plan from './routes/Plan/index' 11 | import Progress from './routes/Progress' 12 | import NotFound from './routes/NotFound' 13 | 14 | // Not real but don't delete. Needed for testing 15 | import TestJig from './routes/TestJig' 16 | 17 | // TODO: Clean up below once we have it all working 18 | import Poc from './routes/Poc' 19 | import Morgan from './routes/Morgan' 20 | 21 | export const routes = new Map(Object.entries({ 22 | // Real routes 23 | '/': {component: Home, allowUnauthenticated: true}, 24 | '/login': {component: Login, allowUnauthenticated: true}, 25 | '/plan': {component: Plan, navbarLabel: 'Plan'}, 26 | '/progress': {component: Progress, navbarLabel: 'Progress'}, 27 | 28 | // Don't delete. Required for Cypress testing 29 | '/test-jig': TestJig, 30 | 31 | // TODO: Clean up below once we know have examples of all 32 | '/poc': {component: Poc, allowUnauthenticated: true}, 33 | '/morgan': Morgan, 34 | 35 | // Don't delete 36 | '*': NotFound, // Catch-all 37 | })) 38 | 39 | debug('routes: %O', routes) 40 | debug('$location: %O', get(location)) 41 | debug('$readyToGo: %O', get(readyToGo)) 42 | 43 | export const activeComponent = derived( 44 | [location, readyToGo], 45 | ([$location, $readyToGo]) => { 46 | debug('Inside activeComponent derivation callback. $location: %O, $readyToGo: %O', $location, $readyToGo) 47 | const routeValue = routes.get($location) 48 | if (!routeValue) { 49 | return routes.get('*').component || routes.get('*') 50 | } 51 | const component = routeValue.component || routeValue 52 | if (routeValue.allowUnauthenticated || $readyToGo === 'ready') { 53 | return component 54 | } else { 55 | return Login 56 | } 57 | }, 58 | Login 59 | ) 60 | -------------------------------------------------------------------------------- /src/routes/Home.svelte: -------------------------------------------------------------------------------- 1 |

Home!

2 | 3 |

This is Home

4 | -------------------------------------------------------------------------------- /src/routes/Login.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |

Hello!

64 |
65 |

66 | 67 | 68 | 69 | 70 |

71 |
72 |
73 |

74 | 75 | 76 | 77 | 78 |

79 |
80 | 81 |
82 |
83 |
84 |
85 | 86 | -------------------------------------------------------------------------------- /src/routes/Morgan.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

Hey, it's Morgan!

7 | 8 |
9 |
10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /src/routes/NotFound.svelte: -------------------------------------------------------------------------------- 1 |

NotFound

2 | 3 |

Oops, this route doesn't exist!

4 | -------------------------------------------------------------------------------- /src/routes/Plan/DoingKanban.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 |
41 | Doing 42 |
43 |
44 | 45 | 46 |
47 |
48 |
Words
49 |
Actions
50 |
Culture
51 |
52 | 53 | 54 | {#each Object.entries(kanbanRestructured) as [queueSwimlaneID, queueSwimlaneContents]} 55 |
56 |
57 | {$queueSwimlanes[queueSwimlaneID].label} 58 |
59 | 60 | 61 | 62 |
63 | {/each} 64 | 65 | -------------------------------------------------------------------------------- /src/routes/Plan/FormulationGrid.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
37 |
38 | {slideLabel} 39 |
40 |
41 | 42 |
43 |
44 | 45 | {#each $formulation.disciplines as discipline} 46 |
47 | {#if slideLabel == 'Todo'} 48 |
49 | {discipline.label} 50 |
51 | {/if} 52 | {#each discipline.practices as practice} 53 | {#if $plan[practice.id].status == slideLabel} 54 |
$openPracticeID = practice.id} on:dragstart={dragStart} on:dragend={dragEnd}> 55 | {practice.label} 56 |
57 | {/if} 58 | {/each} 59 | {#if blankDisciplineIDs[discipline.id]} 60 |
61 | {/if} 62 | {#if slideLabel == 'Done'} 63 |
64 | {discipline.label} 65 |
66 | {/if} 67 |
68 | {/each} 69 | 70 |
71 |
72 | 73 | -------------------------------------------------------------------------------- /src/routes/Plan/KanbanCell.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | {#each kanbanCellContents as practice} 34 | 35 |
$openPracticeID = practice.practice.id} on:dragstart={dragStart} on:dragend={dragEnd}> 36 |
37 |
38 | {practice.practice.label} 39 |
40 |
41 |
42 | {/each} 43 |
44 | 45 | -------------------------------------------------------------------------------- /src/routes/Plan/Plan.svelte: -------------------------------------------------------------------------------- 1 | 96 | 97 |
98 |
99 | {#if $startOn > 0} 100 |
101 | {#if panTimer} 102 | 103 | {:else} 104 | 105 | {/if} 106 |
{slides[$startOn - 1].label}   
107 |
108 | {/if} 109 | 110 | {#if $startOn <= 0 && endOn >= 0} 111 |
112 | 113 |
114 | {/if} 115 | 116 | {#if $startOn <= 1 && endOn >= 1} 117 |
118 | 119 |
120 | {/if} 121 | 122 | {#if $startOn <= 2 && endOn >= 2} 123 |
124 | 125 |
126 | {/if} 127 | 128 | {#if endOn < NUMBER_OF_SLIDES - 1} 129 |
130 | {#if panTimer} 131 | 132 | {:else} 133 | 134 | {/if} 135 |
   {slides[$startOn + 1].label}
136 |
137 | {/if} 138 |
139 |
140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/routes/Plan/PracticeEditor.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 | {#if $openPracticeID} 49 | 50 | 78 | {/if} 79 | 80 | -------------------------------------------------------------------------------- /src/routes/Plan/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './Plan' 2 | -------------------------------------------------------------------------------- /src/routes/Plan/plan-helpers.js: -------------------------------------------------------------------------------- 1 | import {Dragster} from '@matrx/dragster' 2 | import {plan, queueSwimlanes} from '../../stores' 3 | 4 | function findDropZoneParent(target) { 5 | return target.classList.contains('drop-zone') ? target : findDropZoneParent(target.parentNode) 6 | } 7 | 8 | function findPracticeParent(target) { 9 | return target.classList.contains('practice') ? target : findPracticeParent(target.parentNode) 10 | } 11 | 12 | let practiceBeingDragged = null 13 | 14 | export function dragStart(event) { 15 | const practiceParent = findPracticeParent(event.target) 16 | practiceBeingDragged = practiceParent.id 17 | event.target.style.opacity = .5 18 | } 19 | 20 | export function dragEnd(event) { 21 | event.target.style.opacity = "" 22 | } 23 | 24 | export function dragEnter(event) { 25 | // event.preventDefault() 26 | event.target.classList.add('has-background-grey-lighter') 27 | } 28 | 29 | export function dragOver(event) { 30 | event.preventDefault() 31 | } 32 | 33 | export function dragLeave(event) { 34 | event.target.classList.remove('has-background-grey-lighter') 35 | } 36 | 37 | export function drop(event) { 38 | const dropZoneParent = findDropZoneParent(event.target) 39 | dropZoneParent.classList.remove('has-background-grey-lighter') 40 | const queueSwimlaneID = dropZoneParent.getAttribute('queueSwimlaneID') 41 | const assessedLevel = dropZoneParent.getAttribute('assessedLevel') 42 | if (queueSwimlaneID && assessedLevel) { 43 | plan.update((value) => { 44 | value[practiceBeingDragged].queueSwimlaneID = queueSwimlaneID 45 | value[practiceBeingDragged].assessedLevel = assessedLevel 46 | value[practiceBeingDragged].status = 'Doing' 47 | return value 48 | }) 49 | Dragster.reset(dropZoneParent) 50 | } 51 | } 52 | 53 | export function dropPan(event, newStatus) { 54 | const dropZoneParent = findDropZoneParent(event.target) 55 | let queueSwimlanesCached 56 | queueSwimlanes.update((value) => { 57 | queueSwimlanesCached = value 58 | return value 59 | }) 60 | dropZoneParent.classList.remove('has-background-grey-lighter') 61 | plan.update((value) => { 62 | if (newStatus === "Doing") { 63 | value[practiceBeingDragged].queueSwimlaneID = Object.keys(queueSwimlanesCached)[0] 64 | value[practiceBeingDragged].assessedLevel = "Words" 65 | } else { 66 | value[practiceBeingDragged].queueSwimlaneID = null 67 | value[practiceBeingDragged].assessedLevel = null 68 | } 69 | value[practiceBeingDragged].status = newStatus 70 | return value 71 | }) 72 | Dragster.reset(dropZoneParent) 73 | } 74 | -------------------------------------------------------------------------------- /src/routes/Poc.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 | 12 |

POC

13 |
Something to test styles
14 | 15 | 20 | -------------------------------------------------------------------------------- /src/routes/Progress.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 |
25 | {#each levelConfig as level} 26 |
27 |
28 |
29 | 55% 30 |
31 |
32 | 33 |
34 |
{level.label}
35 |
36 |
37 | {/each} 38 |
39 | 40 | 41 |
42 |
43 |

Architecture & Design

44 |
45 |
46 |
47 |
48 |

Orange Belt for Developers (aka Codebashing)

49 |
50 |
51 |
52 | {#each a as column} 53 |
54 |   55 |
56 | {/each} 57 |
58 |
59 |
60 |
61 |
62 |

PIA

63 |
64 |
65 |
66 | {#each a as column} 67 |
68 |   69 |
70 | {/each} 71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /src/routes/Regex.svelte: -------------------------------------------------------------------------------- 1 |

Regex route

2 | 3 |

Match is: {JSON.stringify(params)}

4 | 5 | 8 | -------------------------------------------------------------------------------- /src/routes/TestJig.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |

{$a}

27 | 28 | 29 |

{$aPrime}

30 | 31 | 32 |
33 | 34 |

{$b}

35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const fs = require('fs') 3 | const serveStatic = require('serve-static') 4 | const express = require('express') 5 | const jsonParser = express.json() 6 | const compression = require('compression') 7 | const uuidv4 = require('uuid/v4') 8 | const helmet = require('helmet') 9 | const cookieParser = require('cookie-parser') 10 | const csrf = require('csurf') 11 | const csrfProtection = csrf({cookie: true}) 12 | const debug = require('debug')('matrx:server.js') 13 | 14 | const passport = require('passport') 15 | const LocalStrategy = require('passport-local').Strategy 16 | const expressSession = require('express-session') 17 | const FileStore = require('session-file-store')(expressSession) 18 | 19 | passport.use(new LocalStrategy( 20 | ((username, password, done) => { 21 | debug('LocalStrategy callback called. username: %s', username) 22 | if (password === 'admin') { 23 | return done(null, {id: 1, name: username}) // TODO: Upgrade this with the real information 24 | } else { 25 | return done(null, false) 26 | } 27 | }) 28 | )) 29 | 30 | passport.serializeUser((user, cb) => { 31 | cb(null, user) // TODO: Should probably just stick the userID into the session store 32 | // cb(null, JSON.stringify(user)) 33 | }) 34 | 35 | passport.deserializeUser((packet, cb) => { 36 | // TODO: Lookup in database the user and return it instead of packet 37 | cb(null, packet) 38 | // cb(null, JSON.parse(packet)) 39 | }) 40 | 41 | const {getServer} = require('@matrx/svelte-realtime-server') 42 | const adapters = { 43 | // 'cosmos-db-temporal': require('@matrx/svelte-realtime-adapter-cosmos-db-temporal') 44 | } 45 | 46 | const PORT = process.env.PORT || 8080 47 | const {NODE_ENV, SESSION_SECRET, HOME} = process.env 48 | if (!SESSION_SECRET) { 49 | throw new Error('Must set SESSION_SECRET environment variable') 50 | } 51 | const dev = !(NODE_ENV === 'production') 52 | const sessionPath = (HOME || '/home') + '/sessions' 53 | if (!fs.existsSync(sessionPath)) { 54 | fs.mkdirSync(sessionPath) 55 | } 56 | fs.chmodSync(sessionPath, 0o755) 57 | 58 | const sessionStore = new FileStore({path: sessionPath}) 59 | const app = express() 60 | const server = http.createServer(app) 61 | const svelteRealtimeServer = getServer(server, adapters, sessionStore) 62 | 63 | app.use(expressSession({ 64 | secret: SESSION_SECRET, 65 | resave: false, 66 | saveUninitialized: false, 67 | name: 'sessionID', 68 | store: sessionStore, 69 | cookie: {maxAge: 1000 * 60 * 60 * 24 * 30}, // 30 days 70 | })) 71 | app.use(passport.initialize()) 72 | app.use(passport.session()) 73 | 74 | app.use((req, res, next) => { 75 | res.locals.nonce = uuidv4() 76 | next() 77 | }) 78 | 79 | app.use(helmet({ 80 | contentSecurityPolicy: { 81 | directives: { 82 | scriptSrc: [ 83 | "'self'", 84 | "'unsafe-eval'", 85 | (req, res) => `'nonce-${res.locals.nonce}'` 86 | ], 87 | objectSrc: ["'none'"], 88 | baseUri: ["'self'"] 89 | }, 90 | browserSniff: false 91 | } 92 | })) 93 | 94 | app.use( 95 | compression({threshold: 0}), 96 | serveStatic('dist') 97 | ) 98 | 99 | app.use(cookieParser()) // lgtm [js/missing-token-validation] 100 | 101 | app.post('/login', 102 | (req, res, next) => { 103 | debug('Got POST to /login') 104 | return next() 105 | }, 106 | jsonParser, 107 | (req, res, next) => { 108 | debug('Got POST to /login. req.body: %O', req.body) 109 | return next() 110 | }, 111 | passport.authenticate('local'), 112 | (req, res, next) => { 113 | debug('Login succeeded', req.user) 114 | return res.status(200).json({authenticated: true}) 115 | } 116 | ) 117 | 118 | app.get('/checkauth', 119 | csrfProtection, 120 | (req, res, next) => { 121 | if (req.user) { 122 | return next() 123 | } else { 124 | return res.status(200).json({authenticated: false}) 125 | } 126 | }, 127 | (req, res) => { 128 | return res.status(200).json({authenticated: true}) 129 | } 130 | ) 131 | 132 | app.get('/logout', 133 | csrfProtection, 134 | (req, res, next) => { 135 | debug('Got GET to /logout.\nreq.user: %O\nreq.sessionID: %O', req.user, req.sessionID) 136 | req.session.destroy() 137 | svelteRealtimeServer.logout(req.sessionID) 138 | return res 139 | .status(200) 140 | .clearCookie('sessionID', {httpOnly: true}) 141 | .json({authenticated: false}) 142 | } 143 | ) 144 | 145 | server.listen(PORT, err => { 146 | if (err) { 147 | console.log('error', err) 148 | } else { 149 | console.log('server running on', PORT) 150 | } 151 | }) 152 | -------------------------------------------------------------------------------- /src/stores.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('matrx:stores.js') 2 | 3 | import {writable, derived} from 'svelte/store' 4 | 5 | import {Dragster} from '@matrx/dragster' 6 | 7 | import {ViewstateStore} from '@matrx/svelte-viewstate-store' 8 | import {RealtimeStore} from '@matrx/svelte-realtime-store' 9 | export const connected = RealtimeStore.connected 10 | export const authenticated = writable(false) 11 | // export const readyToGo = writable('not ready') // 'getting ready', 'ready' 12 | export const readyToGo = derived( // 'getting ready', 'ready' 13 | [authenticated, connected], 14 | ([$authenticated, $connected]) => { 15 | debug('Inside readyToGo derivation callback. $authenticated: %O, $connected: %O', $authenticated, $connected) 16 | if ($authenticated && $connected) { 17 | return 'ready' 18 | } else if ($authenticated || $connected) { 19 | return 'getting ready' 20 | } else { 21 | return 'not ready' 22 | } 23 | }, 24 | 'not ready' 25 | ) 26 | 27 | export const openPracticeID = new ViewstateStore({ 28 | identifier: 'openPracticeID', 29 | defaultValue: '', 30 | scope: '/plan', 31 | isGlobal: true 32 | }) 33 | // export const openPracticeID = writable('') 34 | 35 | // export const formulation = writable({ 36 | // export const formulation = realtimeClient.realtime({_entityID: 'formulation'}, { 37 | export const formulation = new RealtimeStore({_entityID: 'formulation', defaultValue: { 38 | label: 'Some formulation', 39 | disciplines: [ 40 | { 41 | id: 'discipline1', 42 | label: 'Artisanship', 43 | description: 'blah', 44 | documentation: 'blah', 45 | practices: [ 46 | { 47 | id: 'practice1', 48 | label: 'Yellow Belt', 49 | description: 'Something to say about **Yellow Belt** in Markdown', 50 | documentation: 'Some _Markdown_ documentation' 51 | }, 52 | { 53 | id: 'practice2', 54 | label: 'Orange Belt', 55 | description: 'blah blah blah', 56 | documentation: 'blah' 57 | } 58 | ] 59 | }, 60 | { 61 | id: 'discipline2', 62 | label: 'Tools', 63 | description: 'blah', 64 | documentation: 'blah', 65 | practices: [ 66 | { 67 | id: 'practice3', 68 | label: 'SCA', 69 | description: 'in Markdown', 70 | documentation: 'Some _Markdown_ documentation' 71 | }, 72 | { 73 | id: 'practice4', 74 | label: 'SAST/IAST', 75 | description: 'Something to say in Markdown', 76 | documentation: 'Some _Markdown_ documentation' 77 | }, 78 | { 79 | id: 'practice5', 80 | label: 'Network Originated Scans', 81 | description: 'blah blah blah', 82 | documentation: 'blah' 83 | }, 84 | { 85 | id: 'practice6', 86 | label: 'Morgans Practice', 87 | description: 'blah blah blah', 88 | documentation: 'blah' 89 | } 90 | ] 91 | } 92 | ] 93 | }}) 94 | // }) 95 | 96 | export const queueSwimlanes = writable({ 97 | queue1: { 98 | id: 'queue1', 99 | label: 'Planned' 100 | }, 101 | queue2: { 102 | id: 'queue2', 103 | label: 'Stretch' 104 | } 105 | }) 106 | 107 | // export const plan = realtimeClient.realtime({_entityID: 'plan'}, { 108 | export const plan = new RealtimeStore({_entityID: 'plan', defaultValue: { 109 | // export const plan = writable({ 110 | practice1: { 111 | practiceID: 'practice1', 112 | formulationID: 'formulation1', 113 | teamID: 'teamA', 114 | assessedLevel: 'Words', 115 | notes: 'Some note', 116 | goalLevel: 'level2', 117 | goalDate: '2020-07-12', 118 | status: 'Doing', 119 | queueSwimlaneID: 'queue1' 120 | }, 121 | practice6: { 122 | practiceID: 'practice6', 123 | formulationID: 'formulation1', 124 | teamID: 'teamA', 125 | assessedLevel: 'Words', 126 | notes: 'Some note', 127 | goalLevel: 'level2', 128 | goalDate: '2020-07-12', 129 | status: 'Doing', 130 | queueSwimlaneID: 'queue1' 131 | }, 132 | practice2: { 133 | practiceID: 'practice2', 134 | formulationID: 'formulation1', 135 | teamID: 'teamA', 136 | assessedLevel: 'Actions', 137 | notes: 'Some note 2', 138 | goalLevel: 'level4', 139 | goalDate: '2020-09-12', 140 | status: 'Doing', 141 | queueSwimlaneID: 'queue1' 142 | }, 143 | practice4: { 144 | practiceID: 'practice4', 145 | formulationID: 'formulation1', 146 | teamID: 'teamA', 147 | assessedLevel: 'Culture', 148 | notes: 'Some note 2', 149 | goalLevel: 'level4', 150 | goalDate: '2020-09-12', 151 | status: 'Doing', 152 | queueSwimlaneID: 'queue2' 153 | }, 154 | practice3: { 155 | practiceID: 'practice3', 156 | formulationID: 'formulation1', 157 | teamID: 'teamA', 158 | assessedLevel: 'Culture', 159 | notes: 'Some note 2', 160 | status: 'Done' 161 | }, 162 | practice5: { 163 | practiceID: 'practice5', 164 | formulationID: 'formulation1', 165 | teamID: 'teamA', 166 | assessedLevel: 'Thoughts', 167 | notes: 'Some note again', 168 | status: 'Todo' 169 | } 170 | }}) 171 | 172 | export function addDragster(node) { 173 | return new Dragster(node) 174 | } 175 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | const sveltePreprocess = require('svelte-preprocess') 2 | 3 | module.exports = { 4 | preprocess: sveltePreprocess({ 5 | postcss: false, 6 | babel: { 7 | presets: [ 8 | [ 9 | '@babel/preset-env', 10 | { 11 | loose: true, 12 | // No need for babel to resolve modules 13 | modules: false, 14 | targets: { 15 | // ! Very important. Target es6+ 16 | esmodules: true, 17 | }, 18 | }, 19 | ], 20 | ], 21 | }, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /to-do.md: -------------------------------------------------------------------------------- 1 | * [X] npm install svelte-spa-router 2 | * [X] Move packages over 3 | * [X] Add express, helmet, etc. and get site to work under that 4 | * [X] Install node-saas. Add Bulma 5 | * [X] Get `npm run dev` to auto build and restart the server 6 | * [X] Get the login and poc pages workings 7 | * [X] Uninstall uneeded servers (express, serve, sirv, live-server, etc.) 8 | * [X] Will notion work in off-line mode? Yes, as an android app 9 | * [X] Add our custom theme Lux and test the build functionality 10 | 11 | * [ ] Move @zeit/cosmosdb-server down into svelte-realtime-adapter... 12 | * [ ] Azure pipeline and/or GitHub actions 13 | * [ ] Create a test comparing routes.js results with contents of src/routes 14 | * [ ] Write one test for MatrX using cypress (or nightwatch?) 15 | * [ ] Write one test for svelte-realtime using cypress (or nightwatch?) 16 | 17 | 18 | -------------------------------------------------------------------------------- /webpack.config.coverage-client.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const path = require('path') 3 | 4 | const mode = process.env.NODE_ENV || 'development' 5 | const prod = mode === 'production' 6 | 7 | const sveltePath = path.resolve('node_modules', 'svelte') 8 | 9 | module.exports = { 10 | entry: { 11 | bundle: ['./src/main.js', './src/matrx.scss'] 12 | }, 13 | resolve: { 14 | alias: { 15 | svelte: sveltePath 16 | }, 17 | extensions: ['.mjs', '.js', '.svelte'], 18 | mainFields: ['svelte', 'browser', 'module', 'main'] 19 | }, 20 | output: { 21 | path: __dirname + '/dist', 22 | filename: '[name].js', 23 | chunkFilename: '[name].[id].js' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.svelte$/, 29 | use: { 30 | loader: 'svelte-loader', 31 | options: { 32 | emitCss: true, 33 | hotReload: true, 34 | preprocess: require('./svelte.config.js').preprocess 35 | } 36 | } 37 | }, 38 | { 39 | test: /\.s[ac]ss$/i, 40 | use: [ 41 | { 42 | loader: MiniCssExtractPlugin.loader, 43 | options: { 44 | // you can specify a publicPath here 45 | // by default it uses publicPath in webpackOptions.output 46 | publicPath: '../dist', 47 | hmr: process.env.NODE_ENV === 'development', 48 | }, 49 | }, 50 | 'css-loader', 51 | 'sass-loader', 52 | ], 53 | }, 54 | { 55 | test: /\.css$/, 56 | use: [ 57 | { 58 | loader: MiniCssExtractPlugin.loader, 59 | options: { 60 | // you can specify a publicPath here 61 | // by default it uses publicPath in webpackOptions.output 62 | publicPath: '../dist', 63 | hmr: process.env.NODE_ENV === 'development', 64 | }, 65 | }, 66 | 'css-loader', 67 | ], 68 | }, 69 | { 70 | test: /\.js$/, 71 | use: { 72 | loader: 'istanbul-instrumenter-loader', 73 | options: {esModules: true} 74 | }, 75 | enforce: 'post', 76 | exclude: /node_modules|cypress/, 77 | } 78 | ] 79 | }, 80 | mode, 81 | plugins: [ 82 | new MiniCssExtractPlugin({ 83 | // Options similar to the same options in webpackOptions.output 84 | // all options are optional 85 | filename: '[name].css', 86 | chunkFilename: '[id].css', 87 | ignoreOrder: false, // Enable to remove warnings about conflicting order 88 | }), 89 | ], 90 | devtool: prod ? false : 'source-map' 91 | } 92 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const path = require('path') 3 | 4 | const mode = process.env.NODE_ENV || 'development' 5 | const prod = mode === 'production' 6 | 7 | const sveltePath = path.resolve('node_modules', 'svelte') 8 | 9 | module.exports = { 10 | entry: { 11 | bundle: ['./src/main.js', './src/matrx.scss'] 12 | // bundle: ['./src/main.js'] 13 | }, 14 | resolve: { 15 | alias: { 16 | svelte: sveltePath 17 | }, 18 | extensions: ['.mjs', '.js', '.svelte'], 19 | mainFields: ['svelte', 'browser', 'module', 'main'] 20 | }, 21 | output: { 22 | path: __dirname + '/dist', 23 | filename: '[name].js', 24 | chunkFilename: '[name].[id].js' 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.svelte$/, 30 | use: { 31 | loader: 'svelte-loader', 32 | options: { 33 | emitCss: true, 34 | hotReload: true, 35 | preprocess: require('./svelte.config.js').preprocess 36 | } 37 | } 38 | }, 39 | { 40 | test: /\.s[ac]ss$/i, 41 | use: [ 42 | { 43 | loader: MiniCssExtractPlugin.loader, 44 | options: { 45 | // you can specify a publicPath here 46 | // by default it uses publicPath in webpackOptions.output 47 | publicPath: '../dist', 48 | hmr: process.env.NODE_ENV === 'development', 49 | }, 50 | }, 51 | 'css-loader', 52 | 'sass-loader', 53 | ], 54 | }, 55 | { 56 | test: /\.css$/, 57 | use: [ 58 | { 59 | loader: MiniCssExtractPlugin.loader, 60 | options: { 61 | // you can specify a publicPath here 62 | // by default it uses publicPath in webpackOptions.output 63 | publicPath: '../dist', 64 | hmr: process.env.NODE_ENV === 'development', 65 | }, 66 | }, 67 | 'css-loader', 68 | ], 69 | }, 70 | ] 71 | }, 72 | mode, 73 | plugins: [ 74 | new MiniCssExtractPlugin({ 75 | // Options similar to the same options in webpackOptions.output 76 | // all options are optional 77 | filename: '[name].css', 78 | chunkFilename: '[id].css', 79 | ignoreOrder: false, // Enable to remove warnings about conflicting order 80 | }), 81 | ], 82 | devtool: prod ? false : 'source-map' 83 | } 84 | --------------------------------------------------------------------------------