feat: Add unittest for webapp using Jest (#541)
* update dependencies packages * fix parse command line option * enable esmodule on jest * define webapp client test job on yamato * define signaling handler on server * setup server test * add jest-websocket-mock * fix handler argument * implement basic test for http/wbsocket signaling handler * fix lint * fix tsconfig.json * change linux vm image for webapp job * fix jest command for win * fix upm-ci-webapp.yml * wait more time for running test server * make client directory for webapp * add cross env for client test * fix httphandler on private mode * fix review
This commit is contained in:
parent
b7e8b08729
commit
4eb8c6052f
|
|
@ -353,6 +353,7 @@ Plugin/webrtc
|
|||
Assets/Samples.meta
|
||||
Assets/Samples/
|
||||
|
||||
|
||||
# Exclude Webapp coverage result
|
||||
WebApp/**/coverage/*
|
||||
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -5,20 +5,24 @@ platforms:
|
|||
flavor: b1.xlarge
|
||||
pack_command: pack_webapp.cmd
|
||||
test_command: test_webapp.cmd
|
||||
client_test_command: test_webapp_client.cmd
|
||||
- name: macos
|
||||
type: Unity::VM::osx
|
||||
image: package-ci/mac:latest
|
||||
flavor: m1.mac
|
||||
pack_command: ./pack_webapp.sh
|
||||
test_command: ./test_webapp.sh
|
||||
client_test_command: ./test_webapp_client.sh
|
||||
- name: linux
|
||||
type: Unity::VM
|
||||
image: cds-ops/ubuntu-18.04-base:stable
|
||||
image: package-ci/ubuntu:latest
|
||||
flavor: b1.xlarge
|
||||
pack_command: ./pack_webapp.sh
|
||||
test_command: ./test_webapp.sh
|
||||
client_test_command: ./test_webapp_client.sh
|
||||
projects:
|
||||
- packagename: com.unity.webapp.renderstreaming
|
||||
- name: renderstreaming
|
||||
packagename: com.unity.webapp.renderstreaming
|
||||
---
|
||||
{% for project in projects %}
|
||||
{% for platform in platforms %}
|
||||
|
|
@ -52,6 +56,22 @@ test_{{ platform.name }}:
|
|||
- "WebApp/coverage/**/*"
|
||||
dependencies:
|
||||
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
|
||||
|
||||
test_client_{{ platform.name }}:
|
||||
name : Test Client {{ project.packagename }} on {{ platform.name }}
|
||||
agent:
|
||||
type: {{ platform.type }}
|
||||
image: {{ platform.image }}
|
||||
flavor: {{ platform.flavor }}
|
||||
commands:
|
||||
- {{ platform.client_test_command }}
|
||||
artifacts:
|
||||
logs:
|
||||
paths:
|
||||
- "WebApp/output.log"
|
||||
- "WebApp/coverage/**/*"
|
||||
dependencies:
|
||||
- .yamato/upm-ci-webapp.yml#pack_{{ platform.name }}
|
||||
{% endfor %}
|
||||
|
||||
trigger_webapp_test_{{ project.name }}:
|
||||
|
|
@ -61,6 +81,7 @@ trigger_webapp_test_{{ project.name }}:
|
|||
dependencies:
|
||||
{% for platform in platforms %}
|
||||
- .yamato/upm-ci-webapp.yml#test_{{ platform.name }}
|
||||
- .yamato/upm-ci-webapp.yml#test_client_{{ platform.name }}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
|
@ -11,6 +11,7 @@ module.exports = {
|
|||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
export default {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/wt/swsbjj0x061bdb0y4dqc0g4c0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
// clearMocks: false,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: "v8",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: [
|
||||
"js",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "jsdom",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "webclient",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^27.2.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 472 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
// test for client
|
||||
|
||||
import { default as Signaling, WebSocketSignaling } from "../public/js/signaling.js";
|
||||
|
||||
test('basic', () => {
|
||||
var http = new Signaling();
|
||||
var ws = new WebSocketSignaling();
|
||||
expect('hello').toBe('hello');
|
||||
});
|
||||
|
||||
test('basic2', () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
|
@ -1,10 +1,196 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
roots: ['<rootDir>/src/', '<rootDir>/test/'],
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/wt/swsbjj0x061bdb0y4dqc0g4c0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
// clearMocks: false,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: "coverage",
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: "v8",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: [
|
||||
"js",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
},
|
||||
testRegex: '(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
coverageDirectory: './coverage/',
|
||||
collectCoverage: true
|
||||
}
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,8 +4,8 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"prestart": "npm install",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "jest --env=node --colors --coverage test",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "jest --colors test/*.ts",
|
||||
"newman": "newman run test/renderstreaming.postman_collection.json",
|
||||
"start": "node ./build/index.js",
|
||||
"dev": "ts-node ./src/index.ts",
|
||||
|
|
@ -13,34 +13,37 @@
|
|||
"pack": "pkg ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/node": "^11.12.0",
|
||||
"@types/ws": "^7.2.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^11.15.54",
|
||||
"@types/ws": "^7.4.7",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.0",
|
||||
"express": "~4.16.4",
|
||||
"morgan": "^1.10.0",
|
||||
"uuid": "^3.4.0",
|
||||
"ws": "^7.4.6"
|
||||
"ws": "^7.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.12",
|
||||
"@types/morgan": "^1.9.2",
|
||||
"@typescript-eslint/eslint-plugin": "^2.3.3",
|
||||
"@typescript-eslint/parser": "^2.3.3",
|
||||
"eslint": "^6.5.1",
|
||||
"jest": "^24.8.0",
|
||||
"newman": "^4.5.5",
|
||||
"pkg": "^4.4.0",
|
||||
"ts-jest": "^24.0.2",
|
||||
"ts-node": "^8.1.0",
|
||||
"typescript": "^3.3.4000"
|
||||
"@jest-mock/express": "^1.4.5",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@typescript-eslint/eslint-plugin": "^2.34.0",
|
||||
"@typescript-eslint/parser": "^2.34.0",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^27.2.0",
|
||||
"jest-websocket-mock": "^2.2.1",
|
||||
"mock-socket": "^9.0.5",
|
||||
"newman": "^5.3.0",
|
||||
"pkg": "^4.5.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
"bin": {
|
||||
"webserver": "build/index.js"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"public/**/*"
|
||||
"client/public/**/*"
|
||||
],
|
||||
"targets": [
|
||||
"node10"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,277 @@
|
|||
import { Request, Response } from 'express';
|
||||
import Offer from './offer';
|
||||
import Answer from './answer';
|
||||
import Candidate from './candidate';
|
||||
|
||||
let isPrivate: boolean;
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
|
||||
|
||||
// [{sessionId:[{connectionId:Offer},...]}]
|
||||
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Answer},...]}]
|
||||
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Candidate},...]}]
|
||||
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
|
||||
|
||||
function getOrCreateConnectionIds(sessionId: string): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(sessionId)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(sessionId, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(sessionId);
|
||||
return connectionIds;
|
||||
}
|
||||
|
||||
function reset(mode: string): void {
|
||||
isPrivate = mode == "private";
|
||||
clients.clear();
|
||||
connectionPair.clear();
|
||||
offers.clear();
|
||||
answers.clear();
|
||||
candidates.clear();
|
||||
}
|
||||
|
||||
function checkSessionId(req: Request, res: Response, next): void {
|
||||
if (req.url === '/') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const id: string = req.header('session-id');
|
||||
if (!clients.has(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function getConnection(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const arrayConnection = Array.from(clients.get(sessionId))
|
||||
const obj = arrayConnection.map((v) => ({ connectionId: v }));
|
||||
res.json({ connections: obj });
|
||||
}
|
||||
|
||||
function getOffer(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
let arrayOffers: [string, Offer][] = [];
|
||||
|
||||
if (offers.size != 0) {
|
||||
if (isPrivate) {
|
||||
if (offers.has(sessionId)) {
|
||||
arrayOffers = Array.from(offers.get(sessionId));
|
||||
}
|
||||
} else {
|
||||
const otherSessionMap = Array.from(offers).filter(x => x[0] != sessionId);
|
||||
arrayOffers = [].concat(...Array.from(otherSessionMap, x => Array.from(x[1], y => [y[0], y[1]])));
|
||||
}
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
|
||||
}
|
||||
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite }));
|
||||
res.json({ offers: obj });
|
||||
}
|
||||
|
||||
function getAnswer(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
let arrayOffers: [string, Answer][] = [];
|
||||
|
||||
if (answers.size != 0 && answers.has(sessionId)) {
|
||||
arrayOffers = Array.from(answers.get(sessionId));
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
|
||||
}
|
||||
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp }));
|
||||
res.json({ answers: obj });
|
||||
}
|
||||
|
||||
function getCandidate(req: Request, res: Response): void {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const connectionIds = Array.from(clients.get(sessionId));
|
||||
const arr = [];
|
||||
for (const connectionId of connectionIds) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair == null) {
|
||||
continue;
|
||||
}
|
||||
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
|
||||
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
|
||||
continue;
|
||||
}
|
||||
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
|
||||
.filter((v) => v.datetime > fromTime)
|
||||
.map((v) => ({ candidate: v.candidate, sdpMLineIndex: v.sdpMLineIndex, sdpMid: v.sdpMid }));
|
||||
if (arrayCandidates.length === 0) {
|
||||
continue;
|
||||
}
|
||||
arr.push({ connectionId, candidates: arrayCandidates });
|
||||
}
|
||||
res.json({ candidates: arr });
|
||||
}
|
||||
|
||||
function createSession(sessionId: string, res: Response): void {
|
||||
clients.set(sessionId, new Set<string>());
|
||||
offers.set(sessionId, new Map<string, Offer>());
|
||||
answers.set(sessionId, new Map<string, Answer>());
|
||||
candidates.set(sessionId, new Map<string, Candidate[]>());
|
||||
res.json({ sessionId: sessionId });
|
||||
}
|
||||
|
||||
function deleteSession(req: Request, res: Response): void {
|
||||
const id: string = req.header('session-id');
|
||||
offers.delete(id);
|
||||
answers.delete(id);
|
||||
candidates.delete(id);
|
||||
clients.delete(id);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function createConnection(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
if (connectionId == null) {
|
||||
res.status(400).send({ error: new Error(`connectionId is required`) });
|
||||
return;
|
||||
}
|
||||
let polite = true;
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
const err = new Error(`${connectionId}: This connection id is already used.`);
|
||||
console.log(err);
|
||||
res.status(400).send({ error: err });
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], sessionId]);
|
||||
const map = getOrCreateConnectionIds(pair[0]);
|
||||
map.add(connectionId);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
res.json({ connectionId: connectionId, polite: polite });
|
||||
}
|
||||
|
||||
function deleteConnection(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
clients.get(sessionId).delete(connectionId);
|
||||
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (otherSessionId) {
|
||||
if (clients.has(otherSessionId)) {
|
||||
clients.get(otherSessionId).delete(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
offers.get(sessionId).delete(connectionId);
|
||||
answers.get(sessionId).delete(connectionId);
|
||||
candidates.get(sessionId).delete(connectionId);
|
||||
res.json({ connectionId: connectionId });
|
||||
}
|
||||
|
||||
function postOffer(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
let keySessionId = null;
|
||||
let polite = false;
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
keySessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (keySessionId != null) {
|
||||
polite = true;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
keySessionId = sessionId;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function postAnswer(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
// add connectionPair
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
|
||||
if (!isPrivate) {
|
||||
connectionPair.set(connectionId, [otherSessionId, sessionId]);
|
||||
}
|
||||
|
||||
const map = answers.get(otherSessionId);
|
||||
map.set(connectionId, new Answer(req.body.sdp, Date.now()));
|
||||
|
||||
// update datetime for candidates
|
||||
const mapCandidates = candidates.get(otherSessionId);
|
||||
if (mapCandidates) {
|
||||
const arrayCandidates = mapCandidates.get(connectionId);
|
||||
if (arrayCandidates) {
|
||||
for (const candidate of arrayCandidates) {
|
||||
candidate.datetime = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
function postCandidate(req: Request, res: Response): void {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
|
||||
const map = candidates.get(sessionId);
|
||||
if (!map.has(connectionId)) {
|
||||
map.set(connectionId, []);
|
||||
}
|
||||
const arr = map.get(connectionId);
|
||||
const candidate = new Candidate(req.body.candidate, req.body.sdpMLineIndex, req.body.sdpMid, Date.now());
|
||||
arr.push(candidate);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
export { reset, checkSessionId, getConnection, getOffer, getAnswer, getCandidate, createSession, deleteSession, createConnection, deleteConnection, postOffer, postAnswer, postCandidate }
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import Offer from './offer';
|
||||
import Answer from './answer';
|
||||
import Candidate from './candidate';
|
||||
|
||||
let isPrivate: boolean;
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<WebSocket, Set<string>> = new Map<WebSocket, Set<string>>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [WebSocket, WebSocket]> = new Map<string, [WebSocket, WebSocket]>();
|
||||
|
||||
function getOrCreateConnectionIds(session: WebSocket): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(session)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(session, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(session);
|
||||
return connectionIds;
|
||||
}
|
||||
|
||||
function reset(mode: string): void {
|
||||
isPrivate = mode == "private";
|
||||
}
|
||||
|
||||
function add(ws: WebSocket): void {
|
||||
clients.set(ws, new Set<string>());
|
||||
}
|
||||
|
||||
function remove(ws: WebSocket): void {
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.forEach(connectionId => {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair) {
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
});
|
||||
|
||||
clients.delete(ws);
|
||||
}
|
||||
|
||||
function onConnect(ws: WebSocket, connectionId: string): void {
|
||||
let polite = true;
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
ws.send(JSON.stringify({ type: "error", message: `${connectionId}: This connection id is already used.` }));
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], ws]);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite }));
|
||||
}
|
||||
|
||||
function onDisconnect(ws: WebSocket, connectionId: string): void {
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.delete(connectionId);
|
||||
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
ws.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
|
||||
function onOffer(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId as string;
|
||||
const newOffer = new Offer(message.sdp, Date.now(), false);
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
newOffer.polite = true;
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
clients.forEach((_v, k) => {
|
||||
if (k == ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
});
|
||||
}
|
||||
|
||||
function onAnswer(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId as string;
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
const newAnswer = new Answer(message.sdp, Date.now());
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
|
||||
if (!isPrivate) {
|
||||
connectionPair.set(connectionId, [otherSessionWs, ws]);
|
||||
}
|
||||
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
|
||||
}
|
||||
|
||||
function onCandidate(ws: WebSocket, message: any): void {
|
||||
const connectionId = message.connectionId;
|
||||
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
|
||||
|
||||
if (isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clients.forEach((_v, k) => {
|
||||
if (k === ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
});
|
||||
}
|
||||
|
||||
export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate }
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { Command } from 'commander';
|
||||
import * as express from 'express';
|
||||
import * as https from 'https';
|
||||
import { Server } from 'http';
|
||||
|
|
@ -10,12 +11,12 @@ import Options from './class/options';
|
|||
|
||||
export class RenderStreaming {
|
||||
public static run(argv: string[]): RenderStreaming {
|
||||
const program = require('commander');
|
||||
const program = new Command();
|
||||
const readOptions = (): Options => {
|
||||
if (Array.isArray(argv)) {
|
||||
program
|
||||
.usage('[options] <apps...>')
|
||||
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || 80)
|
||||
.option('-p, --port <n>', 'Port to start the server on', process.env.PORT || `80`)
|
||||
.option('-s, --secure', 'Enable HTTPS (you need server.key and server.cert)', process.env.SECURE || false)
|
||||
.option('-k, --keyfile <path>', 'https key file (default server.key)', process.env.KEYFILE || 'server.key')
|
||||
.option('-c, --certfile <path>', 'https cert file (default server.cert)', process.env.CERTFILE || 'server.cert')
|
||||
|
|
@ -23,14 +24,15 @@ export class RenderStreaming {
|
|||
.option('-m, --mode <type>', 'Choose Communication mode public or private (default public)', process.env.MODE || 'public')
|
||||
.option('-l, --logging <type>', 'Choose http logging type combined, dev, short, tiny or none.(default dev)', process.env.LOGGING || 'dev')
|
||||
.parse(argv);
|
||||
const option = program.opts();
|
||||
return {
|
||||
port: program.port,
|
||||
secure: program.secure == undefined ? false : program.secure,
|
||||
keyfile: program.keyfile,
|
||||
certfile: program.certfile,
|
||||
websocket: program.websocket == undefined ? false : program.websocket,
|
||||
mode: program.mode,
|
||||
logging: program.logging,
|
||||
port: option.port,
|
||||
secure: option.secure == undefined ? false : option.secure,
|
||||
keyfile: option.keyfile,
|
||||
certfile: option.certfile,
|
||||
websocket: option.websocket == undefined ? false : option.websocket,
|
||||
mode: option.mode,
|
||||
logging: option.logging,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import * as morgan from 'morgan';
|
|||
import signaling from './signaling';
|
||||
import { log, LogLevel } from './log';
|
||||
import Options from './class/options';
|
||||
import { reset as resetHandler }from './class/httphandler';
|
||||
|
||||
export const createServer = (config: Options): express.Application => {
|
||||
const app: express.Application = express();
|
||||
app.set('isPrivate', config.mode == "private");
|
||||
resetHandler(config.mode);
|
||||
// logging http access
|
||||
if (config.logging != "none") {
|
||||
app.use(morgan(config.logging));
|
||||
|
|
@ -19,9 +20,9 @@ export const createServer = (config: Options): express.Application => {
|
|||
app.use(bodyParser.json());
|
||||
app.get('/config', (req, res) => res.json({ useWebSocket: config.websocket, startupMode: config.mode, logging: config.logging }));
|
||||
app.use('/signaling', signaling);
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.use(express.static(path.join(__dirname, '../client/public')));
|
||||
app.get('/', (req, res) => {
|
||||
const indexPagePath: string = path.join(__dirname, '../public/index.html');
|
||||
const indexPagePath: string = path.join(__dirname, '../client/public/index.html');
|
||||
fs.access(indexPagePath, (err) => {
|
||||
if (err) {
|
||||
log(LogLevel.warn, `Can't find file ' ${indexPagePath}`);
|
||||
|
|
|
|||
|
|
@ -1,272 +1,22 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import * as express from 'express';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Offer from './class/offer';
|
||||
import Answer from './class/answer';
|
||||
import Candidate from './class/candidate';
|
||||
import * as handler from'./class/httphandler';
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const router: Router = express.Router();
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<string, Set<string>> = new Map<string, Set<string>>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [string, string]> = new Map<string, [string, string]>(); // key = connectionId
|
||||
|
||||
// [{sessionId:[{connectionId:Offer},...]}]
|
||||
const offers: Map<string, Map<string, Offer>> = new Map<string, Map<string, Offer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Answer},...]}]
|
||||
const answers: Map<string, Map<string, Answer>> = new Map<string, Map<string, Answer>>(); // key = sessionId
|
||||
|
||||
// [{sessionId:[{connectionId:Candidate},...]}]
|
||||
const candidates: Map<string, Map<string, Candidate[]>> = new Map<string, Map<string, Candidate[]>>(); // key = sessionId
|
||||
|
||||
function getOrCreateConnectionIds(sessionId): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(sessionId)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(sessionId, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(sessionId);
|
||||
return connectionIds;
|
||||
}
|
||||
|
||||
router.use((req: Request, res: Response, next) => {
|
||||
if (req.url === '/') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const id: string = req.header('session-id');
|
||||
if (!clients.has(id)) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/connection', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const arrayConnection = Array.from(clients.get(sessionId))
|
||||
const obj = arrayConnection.map((v) => ({ connectionId: v }));
|
||||
res.json({ connections: obj });
|
||||
});
|
||||
|
||||
router.get('/offer', (req: Request, res: Response) => {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
let arrayOffers: [string, Offer][] = [];
|
||||
|
||||
if (offers.size != 0) {
|
||||
if (req.app.get('isPrivate')) {
|
||||
if (offers.has(sessionId)) {
|
||||
arrayOffers = Array.from(offers.get(sessionId));
|
||||
}
|
||||
} else {
|
||||
const otherSessionMap = Array.from(offers).filter(x => x[0] != sessionId);
|
||||
arrayOffers = [].concat(...Array.from(otherSessionMap, x => Array.from(x[1], y => [y[0], y[1]])));
|
||||
}
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
|
||||
}
|
||||
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp, polite: v[1].polite }));
|
||||
res.json({ offers: obj });
|
||||
});
|
||||
|
||||
router.get('/answer', (req: Request, res: Response) => {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
let arrayOffers: [string, Answer][] = [];
|
||||
|
||||
if (answers.size != 0 && answers.has(sessionId)) {
|
||||
arrayOffers = Array.from(answers.get(sessionId));
|
||||
}
|
||||
|
||||
if (fromTime > 0) {
|
||||
arrayOffers = arrayOffers.filter((v) => v[1].datetime > fromTime);
|
||||
}
|
||||
const obj = arrayOffers.map((v) => ({ connectionId: v[0], sdp: v[1].sdp }));
|
||||
res.json({ answers: obj });
|
||||
});
|
||||
|
||||
router.get('/candidate', (req: Request, res: Response) => {
|
||||
// get `fromtime` parameter from request query
|
||||
const fromTime: number = req.query.fromtime ? Number(req.query.fromtime) : 0;
|
||||
const sessionId: string = req.header('session-id');
|
||||
const connectionIds = Array.from(clients.get(sessionId));
|
||||
const arr = [];
|
||||
for (const connectionId of connectionIds) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair == null) {
|
||||
continue;
|
||||
}
|
||||
const otherSessionId = sessionId === pair[0] ? pair[1] : pair[0];
|
||||
if (!candidates.get(otherSessionId) || !candidates.get(otherSessionId).get(connectionId)) {
|
||||
continue;
|
||||
}
|
||||
const arrayCandidates = candidates.get(otherSessionId).get(connectionId)
|
||||
.filter((v) => v.datetime > fromTime)
|
||||
.map((v) => ({ candidate: v.candidate, sdpMLineIndex: v.sdpMLineIndex, sdpMid: v.sdpMid }));
|
||||
if (arrayCandidates.length === 0) {
|
||||
continue;
|
||||
}
|
||||
arr.push({ connectionId, candidates: arrayCandidates });
|
||||
}
|
||||
res.json({ candidates: arr });
|
||||
});
|
||||
|
||||
router.put('', (req: Request, res: Response) => {
|
||||
const id: string = uuid();
|
||||
clients.set(id, new Set<string>());
|
||||
offers.set(id, new Map<string, Offer>());
|
||||
answers.set(id, new Map<string, Answer>());
|
||||
candidates.set(id, new Map<string, Candidate[]>());
|
||||
res.json({ sessionId: id });
|
||||
});
|
||||
|
||||
router.delete('', (req: Request, res: Response) => {
|
||||
const id: string = req.header('session-id');
|
||||
offers.delete(id);
|
||||
answers.delete(id);
|
||||
candidates.delete(id);
|
||||
clients.delete(id);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.put('/connection', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
if (connectionId == null) {
|
||||
res.status(400).send({ error: new Error(`connectionId is required`) });
|
||||
return;
|
||||
}
|
||||
let polite = true;
|
||||
if (req.app.get('isPrivate')) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
const err = new Error(`${connectionId}: This connection id is already used.`);
|
||||
console.log(err);
|
||||
res.status(400).send({ error: err });
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], sessionId]);
|
||||
const map = getOrCreateConnectionIds(pair[0]);
|
||||
map.add(connectionId);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
res.json({ connectionId: connectionId, polite: polite });
|
||||
});
|
||||
|
||||
router.delete('/connection', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
clients.get(sessionId).delete(connectionId);
|
||||
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (otherSessionId) {
|
||||
if (clients.has(otherSessionId)) {
|
||||
clients.get(otherSessionId).delete(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
offers.get(sessionId).delete(connectionId);
|
||||
answers.get(sessionId).delete(connectionId);
|
||||
candidates.get(sessionId).delete(connectionId);
|
||||
res.json({ connectionId: connectionId });
|
||||
});
|
||||
|
||||
router.post('/offer', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
let keySessionId = null;
|
||||
let polite = false;
|
||||
|
||||
if (res.app.get('isPrivate')) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
keySessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
if (keySessionId != null) {
|
||||
polite = true;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
connectionPair.set(connectionId, [sessionId, null]);
|
||||
keySessionId = sessionId;
|
||||
const map = offers.get(keySessionId);
|
||||
map.set(connectionId, new Offer(req.body.sdp, Date.now(), polite))
|
||||
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.post('/answer', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
const connectionIds = getOrCreateConnectionIds(sessionId);
|
||||
connectionIds.add(connectionId);
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
// add connectionPair
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionId = pair[0] == sessionId ? pair[1] : pair[0];
|
||||
|
||||
if (!res.app.get('isPrivate')) {
|
||||
connectionPair.set(connectionId, [otherSessionId, sessionId]);
|
||||
}
|
||||
|
||||
const map = answers.get(otherSessionId);
|
||||
map.set(connectionId, new Answer(req.body.sdp, Date.now()));
|
||||
|
||||
// update datetime for candidates
|
||||
const mapCandidates = candidates.get(otherSessionId);
|
||||
if (mapCandidates) {
|
||||
const arrayCandidates = mapCandidates.get(connectionId);
|
||||
if (arrayCandidates) {
|
||||
for (const candidate of arrayCandidates) {
|
||||
candidate.datetime = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.post('/candidate', (req: Request, res: Response) => {
|
||||
const sessionId: string = req.header('session-id');
|
||||
const { connectionId } = req.body;
|
||||
|
||||
const map = candidates.get(sessionId);
|
||||
if (!map.has(connectionId)) {
|
||||
map.set(connectionId, []);
|
||||
}
|
||||
const arr = map.get(connectionId);
|
||||
const candidate = new Candidate(req.body.candidate, req.body.sdpMLineIndex, req.body.sdpMid, Date.now());
|
||||
arr.push(candidate);
|
||||
res.sendStatus(200);
|
||||
const router: express.Router = express.Router();
|
||||
router.use(handler.checkSessionId);
|
||||
router.get('/connection', handler.getConnection);
|
||||
router.get('/offer', handler.getOffer);
|
||||
router.get('/answer', handler.getAnswer);
|
||||
router.get('/candidate', handler.getCandidate);
|
||||
router.put('', (req: express.Request, res: express.Response) => {
|
||||
const sessionId = uuid();
|
||||
handler.createSession(sessionId, res);
|
||||
});
|
||||
router.delete('', handler.deleteSession);
|
||||
router.put('/connection', handler.createConnection);
|
||||
router.delete('/connection', handler.deleteConnection);
|
||||
router.post('/offer', handler.postOffer);
|
||||
router.post('/answer', handler.postAnswer);
|
||||
router.post('/candidate', handler.postCandidate);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -1,57 +1,25 @@
|
|||
import * as websocket from "ws";
|
||||
import { Server } from 'http';
|
||||
import Offer from './class/offer';
|
||||
import Answer from './class/answer';
|
||||
import Candidate from './class/candidate';
|
||||
|
||||
// [{sessonId:[connectionId,...]}]
|
||||
const clients: Map<WebSocket, Set<string>> = new Map<WebSocket, Set<string>>();
|
||||
|
||||
// [{connectionId:[sessionId1, sessionId2]}]
|
||||
const connectionPair: Map<string, [WebSocket, WebSocket]> = new Map<string, [WebSocket, WebSocket]>();
|
||||
|
||||
function getOrCreateConnectionIds(settion: WebSocket): Set<string> {
|
||||
let connectionIds = null;
|
||||
if (!clients.has(settion)) {
|
||||
connectionIds = new Set<string>();
|
||||
clients.set(settion, connectionIds);
|
||||
}
|
||||
connectionIds = clients.get(settion);
|
||||
return connectionIds;
|
||||
}
|
||||
import * as handler from "./class/websockethandler"
|
||||
|
||||
export default class WSSignaling {
|
||||
server: Server;
|
||||
wss: websocket.Server;
|
||||
isPrivate: boolean;
|
||||
|
||||
constructor(server: Server, mode: string) {
|
||||
this.server = server;
|
||||
this.wss = new websocket.Server({ server });
|
||||
this.isPrivate = mode == "private";
|
||||
handler.reset(mode);
|
||||
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
|
||||
clients.set(ws, new Set<string>());
|
||||
handler.add(ws);
|
||||
|
||||
ws.onclose = (_event: CloseEvent) => {
|
||||
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.forEach(connectionId => {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
if (pair) {
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
});
|
||||
|
||||
clients.delete(ws);
|
||||
ws.onclose = (): void => {
|
||||
handler.remove(ws);
|
||||
}
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
ws.onmessage = (event: MessageEvent): void => {
|
||||
|
||||
// type: connect, disconnect JSON Schema
|
||||
// connectionId: connect or disconnect connectionId
|
||||
|
|
@ -70,19 +38,19 @@ export default class WSSignaling {
|
|||
|
||||
switch (msg.type) {
|
||||
case "connect":
|
||||
this.onConnect(ws, msg.connectionId);
|
||||
handler.onConnect(ws, msg.connectionId);
|
||||
break;
|
||||
case "disconnect":
|
||||
this.onDisconnect(ws, msg.connectionId);
|
||||
handler.onDisconnect(ws, msg.connectionId);
|
||||
break;
|
||||
case "offer":
|
||||
this.onOffer(ws, msg.data);
|
||||
handler.onOffer(ws, msg.data);
|
||||
break;
|
||||
case "answer":
|
||||
this.onAnswer(ws, msg.data);
|
||||
handler.onAnswer(ws, msg.data);
|
||||
break;
|
||||
case "candidate":
|
||||
this.onCandidate(ws, msg.data);
|
||||
handler.onCandidate(ws, msg.data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -90,110 +58,4 @@ export default class WSSignaling {
|
|||
};
|
||||
});
|
||||
}
|
||||
|
||||
private onConnect(ws: WebSocket, connectionId: string) {
|
||||
let polite = true;
|
||||
if (this.isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
|
||||
if (pair[0] != null && pair[1] != null) {
|
||||
ws.send(JSON.stringify({ type: "error", message: `${connectionId}: This connection id is already used.` }));
|
||||
return;
|
||||
} else if (pair[0] != null) {
|
||||
connectionPair.set(connectionId, [pair[0], ws]);
|
||||
}
|
||||
} else {
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
polite = false;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite }));
|
||||
}
|
||||
|
||||
private onDisconnect(ws: WebSocket, connectionId: string) {
|
||||
const connectionIds = clients.get(ws);
|
||||
connectionIds.delete(connectionId);
|
||||
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
}
|
||||
connectionPair.delete(connectionId);
|
||||
ws.send(JSON.stringify({ type: "disconnect", connectionId: connectionId }));
|
||||
}
|
||||
|
||||
private onOffer(ws: WebSocket, message: any) {
|
||||
const connectionId = message.connectionId as string;
|
||||
const newOffer = new Offer(message.sdp, Date.now(), false);
|
||||
|
||||
if (this.isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
newOffer.polite = true;
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
connectionPair.set(connectionId, [ws, null]);
|
||||
clients.forEach((_v, k) => {
|
||||
if (k == ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer }));
|
||||
});
|
||||
}
|
||||
|
||||
private onAnswer(ws: WebSocket, message: any) {
|
||||
const connectionId = message.connectionId as string;
|
||||
const connectionIds = getOrCreateConnectionIds(ws);
|
||||
connectionIds.add(connectionId);
|
||||
const newAnswer = new Answer(message.sdp, Date.now());
|
||||
|
||||
if (!connectionPair.has(connectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
|
||||
if (!this.isPrivate) {
|
||||
connectionPair.set(connectionId, [otherSessionWs, ws]);
|
||||
}
|
||||
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "answer", data: newAnswer }));
|
||||
}
|
||||
|
||||
private onCandidate(ws: WebSocket, message: any) {
|
||||
const connectionId = message.connectionId;
|
||||
const candidate = new Candidate(message.candidate, message.sdpMLineIndex, message.sdpMid, Date.now());
|
||||
|
||||
if (this.isPrivate) {
|
||||
if (connectionPair.has(connectionId)) {
|
||||
const pair = connectionPair.get(connectionId);
|
||||
const otherSessionWs = pair[0] == ws ? pair[1] : pair[0];
|
||||
if (otherSessionWs) {
|
||||
otherSessionWs.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
clients.forEach((_v, k) => {
|
||||
if (k === ws) {
|
||||
return;
|
||||
}
|
||||
k.send(JSON.stringify({ from: connectionId, to: "", type: "candidate", data: candidate }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,154 @@
|
|||
test('basic', () => {
|
||||
expect('hello').toBe('hello');
|
||||
// test for server
|
||||
import { getMockReq, getMockRes } from '@jest-mock/express';
|
||||
import * as httpHandler from '../src/class/httphandler';
|
||||
|
||||
import WS from "jest-websocket-mock";
|
||||
import * as wsHandler from '../src/class/websockethandler';
|
||||
|
||||
describe('http signaling test in public mode', () => {
|
||||
const sessionId = "abcd1234";
|
||||
const headerMock = (): string => sessionId;
|
||||
const connectionId = "12345";
|
||||
|
||||
const { res, mockClear } = getMockRes()
|
||||
const req = getMockReq({ header: headerMock, body: { connectionId: connectionId } });
|
||||
|
||||
beforeAll(() => {
|
||||
httpHandler.reset("public");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClear();
|
||||
});
|
||||
|
||||
test('create session', async () => {
|
||||
await httpHandler.createSession(sessionId, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ sessionId: sessionId });
|
||||
});
|
||||
|
||||
test('create connection', async () => {
|
||||
await httpHandler.createConnection(req, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId, polite: true });
|
||||
});
|
||||
|
||||
test('delete connection', async () => {
|
||||
await httpHandler.deleteConnection(req, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId });
|
||||
});
|
||||
|
||||
test('delete session', async () => {
|
||||
const req = getMockReq({ header: headerMock });
|
||||
await httpHandler.deleteSession(req, res)
|
||||
expect(res.sendStatus).toBeCalledWith(200);
|
||||
});
|
||||
});
|
||||
|
||||
test('basic2', () => {
|
||||
expect(1+1).toBe(2);
|
||||
describe('http signaling test in private mode', () => {
|
||||
const sessionId = "abcd1234";
|
||||
const headerMock = (): string => sessionId;
|
||||
const connectionId = "12345";
|
||||
|
||||
const { res, mockClear } = getMockRes()
|
||||
const req = getMockReq({ header: headerMock, body: { connectionId: connectionId } });
|
||||
|
||||
beforeAll(() => {
|
||||
httpHandler.reset("private");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClear();
|
||||
});
|
||||
|
||||
test('create session', async () => {
|
||||
await httpHandler.createSession(sessionId, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ sessionId: sessionId });
|
||||
});
|
||||
|
||||
test('create connection', async () => {
|
||||
await httpHandler.createConnection(req, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId, polite: false });
|
||||
});
|
||||
|
||||
test('delete connection', async () => {
|
||||
await httpHandler.deleteConnection(req, res);
|
||||
expect(res.json).toHaveBeenCalledWith({ connectionId: connectionId });
|
||||
});
|
||||
|
||||
test('delete session', async () => {
|
||||
const req = getMockReq({ header: headerMock });
|
||||
await httpHandler.deleteSession(req, res)
|
||||
expect(res.sendStatus).toBeCalledWith(200);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('websocket signaling test in public mode', () => {
|
||||
let server: WS;
|
||||
let client: WebSocket;
|
||||
const connectionId = "12345";
|
||||
|
||||
beforeAll(async () => {
|
||||
wsHandler.reset("public");
|
||||
server = new WS("ws://localhost:1234", { jsonProtocol: true });
|
||||
client = new WebSocket("ws://localhost:1234");
|
||||
await server.connected;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
test('create session', async () => {
|
||||
await wsHandler.add(client);
|
||||
});
|
||||
|
||||
test('create connection', async () => {
|
||||
await wsHandler.onConnect(client, connectionId);
|
||||
await expect(server).toReceiveMessage({ type: "connect", connectionId: connectionId, polite: true });
|
||||
});
|
||||
|
||||
test('delete connection', async () => {
|
||||
await wsHandler.onDisconnect(client, connectionId);
|
||||
await expect(server).toReceiveMessage({ type: "disconnect", connectionId: connectionId });
|
||||
});
|
||||
|
||||
test('delete session', async () => {
|
||||
await wsHandler.remove(client);
|
||||
});
|
||||
});
|
||||
|
||||
describe('websocket signaling test in private mode', () => {
|
||||
let server: WS;
|
||||
let client: WebSocket;
|
||||
const connectionId = "12345";
|
||||
|
||||
beforeAll(async () => {
|
||||
wsHandler.reset("private");
|
||||
server = new WS("ws://localhost:1234", { jsonProtocol: true });
|
||||
client = new WebSocket("ws://localhost:1234");
|
||||
await server.connected;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
test('create session', async () => {
|
||||
await wsHandler.add(client);
|
||||
});
|
||||
|
||||
test('create connection', async () => {
|
||||
await wsHandler.onConnect(client, connectionId);
|
||||
await expect(server).toReceiveMessage({ type: "connect", connectionId: connectionId, polite: false });
|
||||
});
|
||||
|
||||
test('delete connection', async () => {
|
||||
await wsHandler.onDisconnect(client, connectionId);
|
||||
await expect(server).toReceiveMessage({ type: "disconnect", connectionId: connectionId });
|
||||
});
|
||||
|
||||
test('delete session', async () => {
|
||||
await wsHandler.remove(client);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"include": ["src/**/*"],
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*", "test/**/*.ts"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ npm install
|
|||
npm run lint
|
||||
npm run test
|
||||
npm run dev -- -p 8080 &
|
||||
sleep 1
|
||||
sleep 5
|
||||
npm run newman -- -e ./test/env_macos.postman_environment.json
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
cd WebApp\client
|
||||
call npm install
|
||||
call npm run test
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
cd WebApp/client
|
||||
npm install
|
||||
npm run test
|
||||
Loading…
Reference in New Issue